diff --git a/.gitignore b/.gitignore index 3808141..134be90 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ # WordPress /wordpress/ /wp-content/ + +# PHPUnit +/.phpunit.result.cache diff --git a/composer.json b/composer.json index 99fe58b..7c2d1ad 100644 --- a/composer.json +++ b/composer.json @@ -39,21 +39,22 @@ }, "require": { "php": ">=5.6.20", + "ext-json": "*", "justinrainbow/json-schema": "^5.2", "pronamic/wp-http": "^1.0", "woocommerce/action-scheduler": "^3.4", - "wp-pay/core": "^4.0" + "wp-pay/core": "^4.3" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", "overtrue/phplint": "^2.3", "php-coveralls/php-coveralls": "^2.4", "phpmd/phpmd": "^2.7", - "phpunit/phpunit": "^5.7 || ^6.0", "pronamic/wp-coding-standards": "^1.0", - "roots/wordpress": "^5.8", + "roots/wordpress": "^5.9", "wp-cli/wp-cli": "^2.3", - "wp-phpunit/wp-phpunit": "^5.8" + "wp-phpunit/wp-phpunit": "^6.0", + "yoast/phpunit-polyfills": "^1.0" }, "scripts": { "ci": [ diff --git a/json-schemas/mandate.json b/json-schemas/mandate.json new file mode 100644 index 0000000..dae81e3 --- /dev/null +++ b/json-schemas/mandate.json @@ -0,0 +1,21 @@ +{ + "$id": "https://github.com/wp-pronamic-pay-mollie/blob/master/json-schemas/mandate.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Mollie mandate.", + "type": "object", + "properties": { + "resource": { + "description": "Indicates the response contains a mandate object. Will always contain `mandate` for this endpoint.", + "type": "string", + "const": "mandate" + }, + "id": { + "description": "The identifier uniquely referring to this mandate.", + "type": "string" + } + }, + "required": [ + "resource", + "id" + ] +} diff --git a/json-schemas/order.json b/json-schemas/order.json new file mode 100644 index 0000000..33806dc --- /dev/null +++ b/json-schemas/order.json @@ -0,0 +1,27 @@ +{ + "$id": "https://github.com/wp-pronamic-pay-mollie/blob/master/json-schemas/order.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Mollie order.", + "type": "object", + "properties": { + "resource": { + "description": "Indicates the response contains an order object. Will always contain `order` for this endpoint.", + "type": "string", + "const": "order" + }, + "id": { + "description": "The order’s unique identifier.", + "type": "string" + }, + "status": { + "description": "The status of the order.", + "type": "string", + "enum": [ "created", "paid", "authorized", "canceled", "shipping", "completed", "expired" ] + } + }, + "required": [ + "resource", + "id", + "status" + ] +} diff --git a/json-schemas/shipment.json b/json-schemas/shipment.json new file mode 100644 index 0000000..bcad978 --- /dev/null +++ b/json-schemas/shipment.json @@ -0,0 +1,21 @@ +{ + "$id": "https://github.com/wp-pronamic-pay-mollie/blob/master/json-schemas/shipment.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Mollie shipment.", + "type": "object", + "properties": { + "resource": { + "description": "Indicates the response contains a shipment object. Will always contain `shipment` for this endpoint.", + "type": "string", + "const": "shipment" + }, + "id": { + "description": "The shipment’s unique identifier.", + "type": "string" + } + }, + "required": [ + "resource", + "id" + ] +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist index b2a5111..b3882e2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,8 +1,6 @@ -includes: - - vendor-bin/phpstan/vendor/szepeviktor/phpstan-wordpress/extension.neon parameters: customRulesetUsed: false - level: max + level: 8 bootstrapFiles: - tests/phpstan/bootstrap.php paths: @@ -10,5 +8,3 @@ parameters: scanDirectories: - vendor/wp-cli - wp-content/plugins/action-scheduler - ignoreErrors: - - '#^Function apply_filters invoked with \d parameters, 2 required\.$#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5c3d159..2d306e0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,26 +1,19 @@ - - - - tests/src - - + + + + src + + - - - src - - + + + tests/src + + - - - + + + diff --git a/src/Address.php b/src/Address.php new file mode 100644 index 0000000..f46627e --- /dev/null +++ b/src/Address.php @@ -0,0 +1,222 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay\Gateways\Mollie + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +use InvalidArgumentException; +use JsonSerializable; +use Pronamic\WordPress\Pay\Address as Core_Address; + +/** + * Address class + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @link https://docs.mollie.com/overview/common-data-types#address-object + */ +class Address implements JsonSerializable { + /** + * The person’s organization, if applicable. + * + * @var string|null + */ + private ?string $organization_name = null; + + /** + * The title of the person, for example Mr. or Mrs. + * + * @var string|null + */ + private ?string $title = null; + + /** + * The given name (first name) of the person. + * + * @var string + */ + private string $given_name; + + /** + * Organization name. + * + * @var string + */ + private string $family_name; + + /** + * The email address of the person. + * + * @var string + */ + private string $email; + + /** + * 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. + * + * @link https://en.wikipedia.org/wiki/E.164 + * @var string|null + */ + private ?string $phone = null; + + /** + * Street and number. + * + * @var string + */ + private string $street_and_number; + + /** + * Additional street details. + * + * @var string|null + */ + private ?string $street_additional = null; + + /** + * Postal code. + * + * @var string|null + */ + private ?string $postal_code = null; + + /** + * City. + * + * @var string + */ + private string $city; + + /** + * Region. + * + * @var string|null + */ + private ?string $region = null; + + /** + * The country of the address in ISO 3166-1 alpha-2 format. + * + * @link https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + * @var string + */ + private string $country; + + /** + * Construct address. + * + * @param string $given_name Given name. + * @param string $family_name Family name. + * @param string $email Email address. + * @param string $street_and_number Street and house number. + * @param string $city City. + * @param string $country Country. + * @throws InvalidArgumentException Throws exception on invalid arguments. + */ + public function __construct( string $given_name, string $family_name, string $email, string $street_and_number, string $city, string $country ) { + /* + * The two-character country code of the address. + * + * The permitted country codes are defined in ISO-3166-1 alpha-2 (e.g. 'NL'). + */ + if ( 2 !== strlen( $country ) ) { + throw new InvalidArgumentException( + sprintf( + 'Given country `%s` not ISO 3166-1 alpha-2 value.', + $country + ) + ); + } + + // Ok. + $this->given_name = $given_name; + $this->family_name = $family_name; + $this->email = $email; + $this->street_and_number = $street_and_number; + $this->city = $city; + $this->country = $country; + } + + /** + * Transform from WordPress Pay core address. + * + * @param Core_Address $address Address. + * @return Address + * @throws InvalidArgumentException Throws exception on invalid arguments. + */ + public static function from_wp_address( Core_Address $address ): Address { + $name = $address->get_name(); + + $given_name = null === $name ? null : $name->get_first_name(); + $family_name = null === $name ? null : $name->get_last_name(); + $email = $address->get_email(); + $street_and_number = $address->get_line_1(); + $city = $address->get_city(); + $country = $address->get_country_code(); + + if ( null === $given_name ) { + throw new InvalidArgumentException( 'Mollie requires a given name in an address.' ); + } + + if ( null === $family_name ) { + throw new InvalidArgumentException( 'Mollie requires a family name in an address.' ); + } + + if ( null === $email ) { + throw new InvalidArgumentException( 'Mollie requires an email in an address.' ); + } + + if ( null === $street_and_number ) { + throw new InvalidArgumentException( 'Mollie requires a street and number in an address.' ); + } + + if ( null === $city ) { + throw new InvalidArgumentException( 'Mollie requires a city in an address.' ); + } + + if ( null === $country ) { + throw new InvalidArgumentException( 'Mollie requires a country in an address.' ); + } + + $mollie_address = new self( $given_name, $family_name, $email, $street_and_number, $city, $country ); + + $mollie_address->organization_name = $address->get_company_name(); + $mollie_address->phone = $address->get_phone(); + $mollie_address->street_additional = $address->get_line_2(); + $mollie_address->postal_code = $address->get_postal_code(); + $mollie_address->region = $address->get_region(); + + return $mollie_address; + } + + /** + * JSON serialize. + * + * @return mixed + */ + public function jsonSerialize() { + $object_builder = new ObjectBuilder(); + + $object_builder->set_optional( 'organizationName', $this->organization_name ); + $object_builder->set_optional( 'title', $this->title ); + $object_builder->set_required( 'givenName', $this->given_name ); + $object_builder->set_required( 'familyName', $this->family_name ); + $object_builder->set_required( 'email', $this->email ); + $object_builder->set_optional( 'phone', $this->phone ); + $object_builder->set_required( 'streetAndNumber', $this->street_and_number ); + $object_builder->set_optional( 'streetAdditional', $this->street_additional ); + $object_builder->set_optional( 'postalCode', $this->postal_code ); + $object_builder->set_required( 'city', $this->city ); + $object_builder->set_optional( 'region', $this->region ); + $object_builder->set_required( 'country', $this->country ); + + return $object_builder->jsonSerialize(); + } +} diff --git a/src/Admin.php b/src/Admin.php index 7f6ba61..365bc5e 100644 --- a/src/Admin.php +++ b/src/Admin.php @@ -11,14 +11,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Title: Mollie admin - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.0.9 - * @since 1.0.0 + * Admin class */ class Admin { /** diff --git a/src/Amount.php b/src/Amount.php index e62e4e7..5d9ed1e 100644 --- a/src/Amount.php +++ b/src/Amount.php @@ -11,18 +11,15 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; use InvalidArgumentException; +use JsonSerializable; use stdClass; use JsonSchema\Constraints\Constraint; use JsonSchema\Validator; /** - * Amount - * - * @author Reüel van der Steege - * @version 2.1.0 - * @since 2.1.0 + * Amount class */ -class Amount { +class Amount implements JsonSerializable { /** * Currency. * @@ -66,38 +63,18 @@ public function get_value() { return $this->value; } - /** - * Get JSON. - * - * @return object - */ - public function get_json() { - return (object) [ - 'currency' => $this->get_currency(), - 'value' => $this->get_value(), - ]; - } - /** * Create amount from object. * * @param stdClass $object Object. - * * @return Amount - * @throws InvalidArgumentException Throws invalid argument exception when object does not contains the required properties. */ public static function from_object( stdClass $object ) { - if ( ! isset( $object->currency ) ) { - throw new InvalidArgumentException( 'Object must contain `currency` property.' ); - } - - if ( ! isset( $object->value ) ) { - throw new InvalidArgumentException( 'Object must contain `value` property.' ); - } + $object_access = new ObjectAccess( $object ); return new self( - $object->currency, - $object->value + $object_access->get_property( 'currency' ), + $object_access->get_property( 'value' ) ); } @@ -130,11 +107,15 @@ public static function from_json( $json ) { } /** - * Create string representation of amount. + * JSON serialize. * - * @return string + * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed */ - public function __toString() { - return sprintf( '%1$s %2$s', $this->get_currency(), $this->get_value() ); + public function jsonSerialize() { + return (object) [ + 'currency' => $this->currency, + 'value' => $this->value, + ]; } } diff --git a/src/AmountTransformer.php b/src/AmountTransformer.php index 8514cb0..6a4d06a 100644 --- a/src/AmountTransformer.php +++ b/src/AmountTransformer.php @@ -13,11 +13,7 @@ use Pronamic\WordPress\Money\Money; /** - * Amount transformer - * - * @author Reüel van der Steege - * @version 2.1.0 - * @since 2.1.0 + * Amount transformer class */ class AmountTransformer { /** diff --git a/src/BaseResource.php b/src/BaseResource.php index c1287dd..12398ef 100644 --- a/src/BaseResource.php +++ b/src/BaseResource.php @@ -11,12 +11,9 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Base Resource + * Base resource class * * @link https://github.com/mollie/mollie-api-php/blob/v2.25.0/src/Resources/BaseResource.php - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 */ abstract class BaseResource { /** diff --git a/src/CLI.php b/src/CLI.php index a315b4b..b30693b 100644 --- a/src/CLI.php +++ b/src/CLI.php @@ -11,14 +11,8 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Title: CLI - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic + * CLI class * - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 * @link https://github.com/woocommerce/woocommerce/blob/3.9.0/includes/class-wc-cli.php */ class CLI { @@ -139,6 +133,7 @@ public function wp_cli_customers_synchronize( $args, $assoc_args ) { ); if ( $query->have_posts() ) { + // @phpstan-ignore-next-line while ( $query->have_posts() ) { $query->the_post(); @@ -218,6 +213,7 @@ public function wp_cli_customers_synchronize( $args, $assoc_args ) { } } + // @phpstan-ignore-next-line \wp_reset_postdata(); } } diff --git a/src/Chargeback.php b/src/Chargeback.php index f68aec6..2172478 100644 --- a/src/Chargeback.php +++ b/src/Chargeback.php @@ -13,11 +13,7 @@ use DateTimeInterface; /** - * Chargeback - * - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 + * Chargeback class */ class Chargeback extends BaseResource { /** @@ -57,6 +53,15 @@ public function get_created_at() { return $this->created_at; } + /** + * Get chargeback amount. + * + * @return Amount + */ + public function get_amount() { + return $this->amount; + } + /** * Create chargeback from JSON. * @@ -76,11 +81,12 @@ public static function from_json( $json ) { \JsonSchema\Constraints\Constraint::CHECK_MODE_EXCEPTIONS ); + $object_access = new ObjectAccess( $json ); + $chargeback = new Chargeback( - $json->id, - Amount::from_json( $json->amount ), - // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object. - new \DateTimeImmutable( $json->createdAt ) + $object_access->get_property( 'id' ), + Amount::from_json( $object_access->get_property( 'amount' ) ), + new \DateTimeImmutable( $object_access->get_property( 'createdAt' ) ) ); return $chargeback; diff --git a/src/Client.php b/src/Client.php index 2d83ba2..4c92a60 100644 --- a/src/Client.php +++ b/src/Client.php @@ -16,14 +16,7 @@ use Pronamic\WordPress\Pay\Core\XML\Security; /** - * Title: Mollie - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.1.4 - * @since 1.0.0 + * Client class */ class Client { /** @@ -52,25 +45,29 @@ public function __construct( $api_key ) { /** * Send request with the specified action and parameters * - * @param string $url URL. - * @param string $method HTTP method to use. - * @param array $data Request data. + * @param string $url URL. + * @param string $method HTTP method to use. + * @param mixed $data Request data. * @return object * @throws Error Throws Error when Mollie error occurs. * @throws \Exception Throws exception when error occurs. */ - public function send_request( $url, $method = 'GET', array $data = [] ) { + public function send_request( $url, $method = 'GET', $data = null ) { // Request. - $response = Http::request( - $url, - [ - 'method' => $method, - 'headers' => [ - 'Authorization' => 'Bearer ' . $this->api_key, - ], - 'body' => $data, - ] - ); + $args = [ + 'method' => $method, + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->api_key, + ], + ]; + + if ( null !== $data ) { + $args['headers']['Content-Type'] = 'application/json'; + + $args['body'] = \wp_json_encode( $data ); + } + + $response = Http::request( $url, $args ); $data = $response->json(); @@ -97,38 +94,62 @@ public function send_request( $url, $method = 'GET', array $data = [] ) { } /** - * Get URL. + * Post data to URL. * - * @param string $endpoint URL endpoint. - * @return string + * @param string $url URL. + * @param mixed $data Data. + * @return object + * @throws Error Throws Error when Mollie error occurs. */ - public function get_url( $endpoint ) { - $url = self::API_URL . $endpoint; - - return $url; + private function post( string $url, $data = null ) { + return $this->send_request( $url, 'POST', $data ); } /** - * Send request to endpoint. + * Get data from URL. * - * @param string $endpoint Endpoint. - * @param string $method HTTP method to use. - * @param array $data Request data. + * @param string $url URL. * @return object + * @throws Error Throws Error when Mollie error occurs. */ - public function send_request_to_endpoint( $endpoint, $method = 'GET', array $data = [] ) { - return $this->send_request( $this->get_url( $endpoint ), $method, $data ); + private function get( string $url ) { + return $this->send_request( $url, 'GET' ); + } + + /** + * Get URL. + * + * @param string $endpoint URL endpoint. + * @param string[] $parts Parts. + * @param string[] $parameters Parameters. + * @return string + */ + private function get_url( $endpoint, array $parts = [], array $parameters = [] ) { + $url = self::API_URL . \strtr( $endpoint, $parts ); + + if ( \count( $parameters ) > 0 ) { + $url .= '?' . \http_build_query( $parameters, '', '&' ); + } + + return $url; } /** * Get profile. * - * @param string $profile Mollie profile ID. + * @param string $profile_id Mollie profile ID. * @return object * @throws Error Throws Error when Mollie error occurs. */ - public function get_profile( $profile ) { - return $this->send_request_to_endpoint( 'profiles/' . $profile, 'GET' ); + public function get_profile( $profile_id ) { + return $this->get( + $this->get_url( + 'profiles/*id*', + [ + '*id*' => $profile_id, + ] + ) + ); } /** @@ -141,6 +162,29 @@ public function get_current_profile() { return $this->get_profile( 'me' ); } + /** + * Create order. + * + * @param OrderRequest $request Order request. + * @return Order + */ + public function create_order( OrderRequest $request ) { + $object = $this->post( + $this->get_url( + 'orders', + [], + [ + 'embed' => 'payments', + ] + ), + $request + ); + + $order = Order::from_json( $object ); + + return $order; + } + /** * Create payment. * @@ -148,20 +192,68 @@ public function get_current_profile() { * @return Payment */ public function create_payment( PaymentRequest $request ) { - $object = $this->send_request_to_endpoint( 'payments', 'POST', $request->get_array() ); + $object = $this->post( + $this->get_url( 'payments' ), + $request + ); $payment = Payment::from_json( $object ); return $payment; } + /** + * Create shipment for an order. + * + * @param string $order_id Order ID. + * @return Shipment + */ + public function create_shipment( $order_id ) { + $response = $this->post( + $this->get_url( + 'orders/*orderId*/shipments', + [ + '*orderId*' => $order_id, + ] + ) + ); + + $shipment = Shipment::from_json( $response ); + + return $shipment; + } + + /** + * Get order. + * + * @param string $order_id Order ID. + * @return Order + */ + public function get_order( string $order_id ) : Order { + $response = $this->get( + $this->get_url( + 'orders/*id*', + [ + '*id*' => $order_id, + ], + [ + 'embed' => 'payments', + ] + ) + ); + + $order = Order::from_json( $response ); + + return $order; + } + /** * Get payments. * * @return bool|object */ public function get_payments() { - return $this->send_request_to_endpoint( 'payments', 'GET' ); + return $this->get( $this->get_url( 'payments' ) ); } /** @@ -177,7 +269,15 @@ public function get_payment( $payment_id, $parameters = [] ) { throw new \InvalidArgumentException( 'Mollie payment ID can not be empty string.' ); } - $object = $this->send_request_to_endpoint( 'payments/' . $payment_id, 'GET', $parameters ); + $object = $this->get( + $this->get_url( + 'payments/*id*', + [ + '*id*' => $payment_id, + ], + $parameters + ) + ); $payment = Payment::from_json( $object ); @@ -190,7 +290,15 @@ public function get_payment( $payment_id, $parameters = [] ) { * @return array */ public function get_issuers() { - $response = $this->send_request_to_endpoint( 'methods/ideal?include=issuers', 'GET' ); + $response = $this->get( + $this->get_url( + 'methods/ideal', + [], + [ + 'include' => 'issuers', + ] + ) + ); $issuers = []; @@ -214,20 +322,27 @@ public function get_issuers() { * Get payment methods * * @param string $sequence_type Sequence type. - * + * @param string $resource Resource type to query, e.g. `payments`, `orders`. * @return array * @throws \Exception Throws exception for methods on failed request or invalid response. */ - public function get_payment_methods( $sequence_type = '' ) { + public function get_payment_methods( $sequence_type = '', $resource = 'payments' ) { $data = [ 'includeWallets' => Methods::APPLE_PAY, + 'resource' => $resource, ]; if ( '' !== $sequence_type ) { $data['sequenceType'] = $sequence_type; } - $response = $this->send_request_to_endpoint( 'methods', 'GET', $data ); + $response = $this->get( + $this->get_url( + 'methods', + [], + $data + ) + ); $payment_methods = []; @@ -260,10 +375,9 @@ public function get_payment_methods( $sequence_type = '' ) { * @since 1.1.6 */ public function create_customer( Customer $customer ) { - $response = $this->send_request_to_endpoint( - 'customers', - 'POST', - $customer->get_array() + $response = $this->post( + $this->get_url( 'customers' ), + $customer ); if ( \property_exists( $response, 'id' ) ) { @@ -288,7 +402,14 @@ public function get_customer( $customer_id ) { } try { - return $this->send_request_to_endpoint( 'customers/' . $customer_id, 'GET' ); + return $this->get( + $this->get_url( + 'customers/*id*', + [ + '*id*' => $customer_id, + ] + ) + ); } catch ( Error $error ) { if ( 404 === $error->get_status() ) { return null; @@ -303,14 +424,17 @@ public function get_customer( $customer_id ) { * * @param string $customer_id Customer ID. * @param BankAccountDetails $consumer_bank_details Consumer bank details. - * @return object - * @throws Error Throws Error when Mollie error occurs. - * @since unreleased + * @return Mandate + * @throws \Exception Throws exception when mandate creation failed. */ public function create_mandate( $customer_id, BankAccountDetails $consumer_bank_details ) { - $response = $this->send_request_to_endpoint( - 'customers/' . $customer_id . '/mandates', - 'POST', + $response = $this->post( + $this->get_url( + 'customers/*customerId*/mandates', + [ + '*customerId*' => $customer_id, + ] + ), [ 'method' => Methods::DIRECT_DEBIT, 'consumerName' => $consumer_bank_details->get_name(), @@ -318,7 +442,7 @@ public function create_mandate( $customer_id, BankAccountDetails $consumer_bank_ ] ); - return $response; + return Mandate::from_json( $response ); } /** @@ -338,7 +462,15 @@ public function get_mandate( $mandate_id, $customer_id ) { throw new \InvalidArgumentException( 'Mollie customer ID can not be empty string.' ); } - return $this->send_request_to_endpoint( 'customers/' . $customer_id . '/mandates/' . $mandate_id, 'GET' ); + return $this->get( + $this->get_url( + 'customers/*customerId*/mandates/*id*', + [ + '*customerId*' => $customer_id, + '*id*' => $mandate_id, + ] + ) + ); } /** @@ -353,7 +485,17 @@ public function get_mandates( $customer_id ) { throw new \InvalidArgumentException( 'Mollie customer ID can not be empty string.' ); } - return $this->send_request_to_endpoint( 'customers/' . $customer_id . '/mandates?limit=250', 'GET' ); + return $this->get( + $this->get_url( + 'customers/*customerId*/mandates', + [ + '*customerId*' => $customer_id, + ], + [ + 'limit' => '250', + ] + ) + ); } /** @@ -466,7 +608,15 @@ public function get_first_valid_mandate_datetime( $customer_id, $payment_method * @return Refund */ public function create_refund( $payment_id, RefundRequest $refund_request ) { - $response = $this->send_request_to_endpoint( 'payments/' . $payment_id . '/refunds', 'POST', $refund_request->get_array() ); + $response = $this->post( + $this->get_url( + 'payments/*id*/refunds', + [ + '*id*' => $payment_id, + ] + ), + $refund_request + ); return Refund::from_json( $response ); } @@ -479,7 +629,15 @@ public function create_refund( $payment_id, RefundRequest $refund_request ) { * @return array */ public function get_payment_chargebacks( $payment_id, $parameters ) { - $object = $this->send_request_to_endpoint( 'payments/' . $payment_id . '/chargebacks', 'GET', $parameters ); + $object = $this->get( + $this->get_url( + 'payments/*paymentId*/chargebacks', + [ + '*paymentId*' => $payment_id, + ], + $parameters + ) + ); $chargebacks = []; diff --git a/src/Config.php b/src/Config.php index b2eb060..64e80af 100644 --- a/src/Config.php +++ b/src/Config.php @@ -13,14 +13,7 @@ use Pronamic\WordPress\Pay\Core\GatewayConfig; /** - * Title: Mollie config - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.0.9 - * @since 1.0.0 + * Config class */ class Config extends GatewayConfig { /** diff --git a/src/Customer.php b/src/Customer.php index b880910..4da6e5e 100644 --- a/src/Customer.php +++ b/src/Customer.php @@ -10,15 +10,14 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; +use JsonSerializable; + /** - * Mollie customer + * Customer class * - * @link https://docs.mollie.com/reference/v2/customers-api/create-customer - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 + * @link https://docs.mollie.com/reference/v2/customers-api/create-customer */ -class Customer { +class Customer implements JsonSerializable { /** * ID. * @@ -159,23 +158,18 @@ public function set_locale( $locale ) { } /** - * Get array. + * JSON serialize. * - * @return array + * @return mixed */ - public function get_array() { - $array = [ - 'name' => $this->get_name(), - 'email' => $this->get_email(), - 'locale' => $this->get_locale(), - ]; + public function jsonSerialize() { + $object_builder = new ObjectBuilder(); - /* - * Array filter will remove values NULL, FALSE and empty strings ('') - */ - $array = array_filter( $array ); + $object_builder->set_optional( 'name', $this->get_name() ); + $object_builder->set_optional( 'email', $this->get_email() ); + $object_builder->set_optional( 'locale', $this->get_locale() ); - return $array; + return $object_builder->jsonSerialize(); } /** @@ -187,25 +181,13 @@ public function get_array() { public static function from_object( $object ) { $customer = new self(); - if ( property_exists( $object, 'id' ) ) { - $customer->set_id( $object->id ); - } - - if ( property_exists( $object, 'mode' ) ) { - $customer->set_mode( $object->mode ); - } - - if ( property_exists( $object, 'name' ) ) { - $customer->set_name( $object->name ); - } - - if ( property_exists( $object, 'email' ) ) { - $customer->set_email( $object->email ); - } + $object_access = new ObjectAccess( $object ); - if ( property_exists( $object, 'locale' ) ) { - $customer->set_locale( $object->locale ); - } + $customer->set_id( $object_access->get_optional( 'id' ) ); + $customer->set_mode( $object_access->get_optional( 'mode' ) ); + $customer->set_name( $object_access->get_optional( 'name' ) ); + $customer->set_email( $object_access->get_optional( 'email' ) ); + $customer->set_locale( $object_access->get_optional( 'locale' ) ); return $customer; } diff --git a/src/CustomerDataStore.php b/src/CustomerDataStore.php index c9b11fd..08c3e21 100644 --- a/src/CustomerDataStore.php +++ b/src/CustomerDataStore.php @@ -11,14 +11,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Title: Mollie customer data store - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 + * Customer data store class */ class CustomerDataStore { /** diff --git a/src/CustomerQuery.php b/src/CustomerQuery.php index 43a8c84..333faac 100644 --- a/src/CustomerQuery.php +++ b/src/CustomerQuery.php @@ -11,14 +11,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Title: Mollie customer query - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 + * Customer query class */ class CustomerQuery { /** diff --git a/src/Error.php b/src/Error.php index 66bf474..a4a5d21 100644 --- a/src/Error.php +++ b/src/Error.php @@ -11,12 +11,9 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Mollie error + * Error class * - * @link https://docs.mollie.com/guides/handling-errors - * @author Remco Tolsma - * @version 2.0.9 - * @since 2.0.9 + * @link https://docs.mollie.com/guides/handling-errors */ class Error extends \Exception { /** diff --git a/src/Gateway.php b/src/Gateway.php index ad5cb07..4c9fdcb 100644 --- a/src/Gateway.php +++ b/src/Gateway.php @@ -10,6 +10,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; +use InvalidArgumentException; use Pronamic\WordPress\DateTime\DateTime; use Pronamic\WordPress\Money\Money; use Pronamic\WordPress\Pay\Banks\BankAccountDetails; @@ -24,14 +25,7 @@ use Pronamic\WordPress\Pay\Subscriptions\SubscriptionStatus; /** - * Title: Mollie - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.1.4 - * @since 1.1.0 + * Gateway class */ class Gateway extends Core_Gateway { /** @@ -70,7 +64,7 @@ class Gateway extends Core_Gateway { public function __construct( Config $config ) { $this->config = $config; - parent::__construct( $config ); + parent::__construct(); $this->set_method( self::METHOD_HTTP_REDIRECT ); @@ -125,31 +119,33 @@ public function get_issuers() { public function get_available_payment_methods() { $payment_methods = []; - // Set sequence types to get payment methods for. + $resources = [ ResourceType::PAYMENTS, ResourceType::ORDERS ]; $sequence_types = [ Sequence::ONE_OFF, Sequence::RECURRING, Sequence::FIRST ]; $results = []; - foreach ( $sequence_types as $sequence_type ) { - // Get active payment methods for Mollie account. - $result = $this->client->get_payment_methods( $sequence_type ); + foreach ( $resources as $resource ) { + foreach ( $sequence_types as $sequence_type ) { + // Get active payment methods for Mollie account. + $result = $this->client->get_payment_methods( $sequence_type, $resource ); - if ( Sequence::FIRST === $sequence_type ) { - foreach ( $result as $method => $title ) { - unset( $result[ $method ] ); + if ( Sequence::FIRST === $sequence_type ) { + foreach ( $result as $method => $title ) { + unset( $result[ $method ] ); - // Get WordPress payment method for direct debit method. - $method = Methods::transform_gateway_method( $method ); - $payment_method = array_search( $method, PaymentMethods::get_recurring_methods(), true ); + // Get WordPress payment method for direct debit method. + $method = Methods::transform_gateway_method( $method ); + $payment_method = array_search( $method, PaymentMethods::get_recurring_methods(), true ); - if ( $payment_method ) { - $results[ $payment_method ] = $title; + if ( $payment_method ) { + $results[ $payment_method ] = $title; + } } } - } - if ( is_array( $result ) ) { - $results = array_merge( $results, $result ); + if ( is_array( $result ) ) { + $results = array_merge( $results, $result ); + } } } @@ -194,6 +190,9 @@ public function get_supported_payment_methods() { PaymentMethods::GIROPAY, PaymentMethods::IDEAL, PaymentMethods::KBC, + PaymentMethods::KLARNA_PAY_LATER, + PaymentMethods::KLARNA_PAY_NOW, + PaymentMethods::KLARNA_PAY_OVER_TIME, PaymentMethods::PAYPAL, PaymentMethods::PRZELEWY24, PaymentMethods::SOFORT, @@ -205,13 +204,35 @@ public function get_supported_payment_methods() { * * @param Payment $payment Payment. * @return string|null + * @throws \Exception Throws exception when resource to use for payment is unknown. */ public function get_webhook_url( Payment $payment ) { - $url = \rest_url( Integration::REST_ROUTE_NAMESPACE . '/webhook/' . (string) $payment->get_id() ); + $resource = $this->get_resource_for_payment( $payment ); + + switch ( $resource ) { + case ResourceType::ORDERS: + $path = '/orders/webhook/'; + break; + case ResourceType::PAYMENTS: + $path = '/payments/webhook/'; + break; + default: + throw new \Exception( \sprintf( 'Unknown resource for payment: %s.', $resource ) ); + } + + $path = \strtr( + $path, + [ + '' => Integration::REST_ROUTE_NAMESPACE, + '' => $payment->get_id(), + ] + ); - $host = wp_parse_url( $url, PHP_URL_HOST ); + $url = \rest_url( $path ); - if ( is_array( $host ) ) { + $host = \wp_parse_url( $url, PHP_URL_HOST ); + + if ( ! \is_string( $host ) ) { // Parsing failure. $host = ''; } @@ -239,15 +260,126 @@ public function get_webhook_url( Payment $payment ) { * @see Core_Gateway::start() * @param Payment $payment Payment. * @return void - * @throws Error Mollie error. * @throws \Exception Throws exception on error creating Mollie customer for payment. */ public function start( Payment $payment ) { + $resource = $this->get_resource_for_payment( $payment ); + + switch ( $resource ) { + case ResourceType::ORDERS: + $this->start_order( $payment ); + + break; + case ResourceType::PAYMENTS: + $this->start_payment( $payment ); + + break; + default: + throw new \Exception( \sprintf( 'Unknown resource for payment: %s.', $resource ) ); + } + } + + /** + * Start Mollie payment for payment. + * + * @param Payment $payment Payment. + * @return void + * @throws Error Throws Mollie error when something goes wrong. + */ + private function start_payment( Payment $payment ) { + $request = $this->get_payment_request( $payment ); + + // Create payment. + $attempt = (int) $payment->get_meta( 'mollie_create_payment_attempt' ); + $attempt = empty( $attempt ) ? 1 : $attempt + 1; + + $payment->set_meta( 'mollie_create_payment_attempt', $attempt ); + + try { + $mollie_payment = $this->client->create_payment( $request ); + + $payment->delete_meta( 'mollie_create_payment_attempt' ); + } catch ( Error $error ) { + if ( 'recurring' !== $request->get_sequence_type() ) { + throw $error; + } + + if ( null === $request->get_mandate_id() ) { + throw $error; + } + + /** + * Only schedule retry for specific status codes. + * + * @link https://docs.mollie.com/overview/handling-errors + */ + if ( ! \in_array( $error->get_status(), [ 429, 502, 503 ], true ) ) { + throw $error; + } + + \as_schedule_single_action( + \time() + $this->get_retry_seconds( $attempt ), + 'pronamic_pay_mollie_payment_start', + [ + 'payment_id' => $payment->get_id(), + ], + 'pronamic-pay-mollie' + ); + + // Add note. + $payment->add_note( + \sprintf( + '%s - %s - %s', + $error->get_status(), + $error->get_title(), + $error->get_detail() + ) + ); + + $payment->save(); + + return; + } + + $this->update_payment_from_mollie_payment( $payment, $mollie_payment ); + } + + /** + * Start Mollie order for payment. + * + * @param Payment $payment Payment. + * @return void + */ + private function start_order( Payment $payment ) { + $request = $this->get_order_request( $payment ); + + $mollie_order = $this->client->create_order( $request ); + + $payment->set_meta( 'mollie_order_id', $mollie_order->get_id() ); + + $order_payments = $mollie_order->get_payments(); + + if ( null !== $order_payments ) { + $mollie_payment = reset( $order_payments ); + + if ( $mollie_payment instanceof MolliePayment ) { + $this->update_payment_from_mollie_payment( $payment, $mollie_payment ); + } + } + } + + /** + * Get Mollie payment request for the specified payment. + * + * @param Payment $payment Payment. + * @return PaymentRequest + */ + private function get_payment_request( Payment $payment ) { $description = (string) $payment->get_description(); /** * Filters the Mollie payment description. - * + * * The maximum length of the description field differs per payment * method, with the absolute maximum being 255 characters. * @@ -317,8 +449,16 @@ public function start( Payment $payment ) { if ( \count( $subscriptions ) > 0 - || - PaymentMethods::is_direct_debit_method( $payment_method ) + || + \in_array( + $payment_method, + [ + PaymentMethods::DIRECT_DEBIT_BANCONTACT, + PaymentMethods::DIRECT_DEBIT_IDEAL, + PaymentMethods::DIRECT_DEBIT_SOFORT, + ], + true + ) ) { $request->set_sequence_type( 'first' ); @@ -369,11 +509,7 @@ public function start( Payment $payment ) { if ( false === $mandate_id ) { $mandate = $this->client->create_mandate( $customer_id, $consumer_bank_details ); - if ( ! \property_exists( $mandate, 'id' ) ) { - throw new \Exception( 'Missing mandate ID.' ); - } - - $mandate_id = $mandate->id; + $mandate_id = $mandate->get_id(); } // Charge immediately on-demand. @@ -415,7 +551,7 @@ public function start( Payment $payment ) { // Billing email. $billing_email = ( null === $customer ) ? null : $customer->get_email(); - /** + /** * Filters the Mollie payment billing email used for bank transfer payment instructions. * * @since 2.2.0 @@ -438,60 +574,113 @@ public function start( Payment $payment ) { $request->set_due_date( $due_date ); } - // Create payment. - $attempt = (int) $payment->get_meta( 'mollie_create_payment_attempt' ); - $attempt = empty( $attempt ) ? 1 : $attempt + 1; + return $request; + } - $payment->set_meta( 'mollie_create_payment_attempt', $attempt ); + /** + * Get Mollie order request. + * + * @param Payment $payment Payment. + * @return OrderRequest + * @throws InvalidArgumentException Throws an invalid argument exception when the payment does not meet the Mollie requirements for an order. + */ + private function get_order_request( Payment $payment ) { + $payment_request = $this->get_payment_request( $payment ); - try { - $mollie_payment = $this->client->create_payment( $request ); + $lines = $payment->get_lines(); - $payment->delete_meta( 'mollie_create_payment_attempt' ); - } catch ( Error $error ) { - if ( 'recurring' !== $request->get_sequence_type() ) { - throw $error; - } + if ( null === $lines ) { + throw new InvalidArgumentException( 'Mollie requires lines for order.' ); + } - if ( null === $request->get_mandate_id() ) { - throw $error; - } + $order_number = $payment->get_source_id(); - /** - * Only schedule retry for specific status codes. - * - * @link https://docs.mollie.com/overview/handling-errors - */ - if ( ! \in_array( $error->get_status(), [ 429, 502, 503 ], true ) ) { - throw $error; - } + if ( null === $order_number ) { + throw new InvalidArgumentException( 'Mollie requires order number for order.' ); + } - \as_schedule_single_action( - \time() + $this->get_retry_seconds( $attempt ), - 'pronamic_pay_mollie_payment_start', - [ - 'payment_id' => $payment->get_id(), - ], - 'pronamic-pay-mollie' - ); + if ( null === $payment_request->locale ) { + throw new InvalidArgumentException( 'Mollie requires locale for order.' ); + } - // Add note. - $payment->add_note( - \sprintf( - '%s - %s - %s', - $error->get_status(), - $error->get_title(), - $error->get_detail() - ) - ); + $order_request = new OrderRequest( + $payment_request->amount, + (string) $order_number, + Lines::from_wp_payment_lines( $lines ), + $payment_request->locale + ); - $payment->save(); + $order_request->redirect_url = $payment->get_return_url(); + $order_request->webhook_url = $this->get_webhook_url( $payment ); + $order_request->method = $payment_request->method; - return; + // Billing address. + $billing_address = $payment->get_billing_address(); + + $order_request->set_billing_address( null === $billing_address ? null : Address::from_wp_address( $billing_address ) ); + + // Shipping address. + $shipping_address = $payment->get_shipping_address(); + + $order_request->set_shipping_address( null === $shipping_address ? null : Address::from_wp_address( $shipping_address ) ); + + // Consumer date of birth. + $customer = $payment->get_customer(); + + $consumer_date_of_birth = null === $customer ? null : $customer->get_birth_date(); + + $order_request->set_consumer_date_of_birth( $consumer_date_of_birth ); + + // Payment. + $order_request->payment = \array_filter( + [ + 'issuer' => $payment_request->issuer, + 'customerId' => $payment_request->customer_id, + 'mandateId' => $payment_request->mandate_id, + 'sequenceType' => $payment_request->sequence_type, + 'webhookUrl' => $payment_request->webhook_url, + ] + ); + + return $order_request; + } + + /** + * Determine if an order should be created for the payment. + * + * @param Payment $payment Payment. + * @return string + */ + private function get_resource_for_payment( Payment $payment ) : string { + $resource = ResourceType::PAYMENTS; + + $is_memberpress = ( 'memberpress_transaction' === $payment->get_source() ); + + $is_klarna = \in_array( + $payment->get_payment_method(), + [ + PaymentMethods::KLARNA_PAY_NOW, + PaymentMethods::KLARNA_PAY_LATER, + PaymentMethods::KLARNA_PAY_OVER_TIME, + ], + true + ); + + if ( $is_memberpress && $is_klarna ) { + $resource = ResourceType::ORDERS; } - // Update payment from Mollie payment. - $this->update_payment_from_mollie_payment( $payment, $mollie_payment ); + /** + * Filters the resource to use for the payment. + * + * @link https://docs.mollie.com/reference/v2/payments-api/create-payment#parameters + * @since 4.0.0 + * @param string $resource Resource. + * @param Payment $payment Payment. + */ + $resource = \apply_filters( 'pronamic_pay_mollie_resource_for_payment', $resource, $payment ); + + return $resource; } /** @@ -531,6 +720,72 @@ public function update_status( Payment $payment ) { $mollie_payment = $this->client->get_payment( $transaction_id ); $this->update_payment_from_mollie_payment( $payment, $mollie_payment ); + + $this->maybe_create_shipment_for_payment( $payment ); + } + + /** + * Maybe create shipment for payment. + * + * @param Payment $payment Payment. + */ + public function maybe_create_shipment_for_payment( Payment $payment ) : void { + $mollie_order_id = $payment->get_meta( 'mollie_order_id' ); + + if ( empty( $mollie_order_id ) ) { + return; + } + + $order = $this->client->get_order( $mollie_order_id ); + + // Check order status. + if ( ! \in_array( $order->get_status(), [ Statuses::AUTHORIZED, Statuses::PAID ], true ) ) { + return; + } + + // Update payment to successful order payment. + $mollie_payments = $order->get_payments(); + + if ( null === $mollie_payments ) { + return; + } + + $transaction_id = $payment->get_transaction_id(); + + foreach ( $mollie_payments as $mollie_payment ) { + if ( ! \in_array( $order->get_status(), [ Statuses::AUTHORIZED, Statuses::PAID ] ) ) { + continue; + } + + $mollie_payment_id = $mollie_payment->get_id(); + + if ( $mollie_payment_id !== $transaction_id ) { + $payment->set_transaction_id( $mollie_payment->get_id() ); + + $payment->add_note( + \sprintf( + /* translators: 1: payment transaction ID, 2: Mollie payment ID */ + \__( 'Payment transaction ID updated from `%1$s` to successful order payment `%2$s`.', 'pronamic_ideal' ), + $transaction_id, + $mollie_payment_id + ) + ); + + $this->update_payment_from_mollie_payment( $payment, $mollie_payment ); + } + } + + // Create shipment and add payment note. + $shipment = $this->client->create_shipment( $mollie_order_id ); + + $payment->add_note( + \sprintf( + /* translators: 1: payment transaction ID, 2: Mollie payment ID */ + \__( 'Shipment `%1$s` created for Mollie order `%2$s`.', 'pronami c_ideal' ), + $shipment->get_id(), + $mollie_order_id + ) + ); } /** diff --git a/src/Install.php b/src/Install.php index 71de265..aac22a0 100644 --- a/src/Install.php +++ b/src/Install.php @@ -11,14 +11,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Title: Mollie install. - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 + * Install class */ class Install { /** diff --git a/src/Integration.php b/src/Integration.php index cf51861..35b20d0 100644 --- a/src/Integration.php +++ b/src/Integration.php @@ -16,14 +16,7 @@ use Pronamic\WordPress\Pay\Subscriptions\Subscription as CoreSubscription; /** - * Title: Mollie integration - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.1.4 - * @since 1.0.0 + * Integration class */ class Integration extends AbstractGatewayIntegration { /** @@ -43,7 +36,7 @@ class Integration extends AbstractGatewayIntegration { /** * Construct and initialize Mollie integration. * - * @param array $args Arguments. + * @param array $args Arguments. */ public function __construct( $args = [] ) { $args = wp_parse_args( diff --git a/src/Line.php b/src/Line.php new file mode 100644 index 0000000..2aabe5c --- /dev/null +++ b/src/Line.php @@ -0,0 +1,209 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay\Gateways\Mollie + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +use JsonSerializable; +use Pronamic\WordPress\Number\Number; + +/** + * Line class + */ +class Line implements JsonSerializable { + /** + * The type of product bought, for example, a physical or a digital product. + * + * @see LineType + * @var string|null + */ + private $type; + + /** + * The category of product bought. + * + * Optional, but required in at least one of the lines to accept `voucher` payments. + * + * @var string|null + */ + private $category; + + /** + * Name. + * + * @var string + */ + private $name; + + /** + * Quantity. + * + * @var int + */ + private $quantity; + + /** + * The price of a single item including VAT in the order line. + * + * @var Amount + */ + private $unit_price; + + /** + * 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. + * + * @var Amount|null + */ + private $discount_amount; + + /** + * 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. + * + * The total amount should match the following formula: (unitPrice × quantity) - discountAmount + * + * @var Amount + */ + private $total_amount; + + /** + * 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. + * + * @var Number + */ + private $vat_rate; + + /** + * 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))`. + * + * @var Amount + */ + private $vat_amount; + + /** + * SKU. + * + * @var string|null + */ + private $sku; + + /** + * Image url. + * + * @var string|null + */ + private $image_url; + + /** + * Product URL. + * + * @var string|null + */ + private $product_url; + + /** + * Line constructor. + * + * @param string $name Description of the order line. + * @param int $quantity Quantity. + * @param Amount $unit_price Unit price. + * @param Amount $total_amount Total amount, including VAT and discounts. + * @param Number $vat_rate VAT rate. + * @param Amount $vat_amount Value-added tax amount. + */ + public function __construct( string $name, int $quantity, Amount $unit_price, Amount $total_amount, Number $vat_rate, Amount $vat_amount ) { + $this->name = $name; + $this->quantity = $quantity; + $this->unit_price = $unit_price; + $this->total_amount = $total_amount; + $this->vat_rate = $vat_rate; + $this->vat_amount = $vat_amount; + } + + /** + * Set type. + * + * @param string|null $type Type. + */ + public function set_type( ?string $type ) : void { + $this->type = $type; + } + + /** + * Set category. + * + * @param null|string $category Product category. + */ + public function set_category( ?string $category ) : void { + $this->category = $category; + } + + /** + * Set discount amount, should not contain any tax. + * + * @param Amount|null $discount_amount Discount amount. + */ + public function set_discount_amount( ?Amount $discount_amount = null ) : void { + $this->discount_amount = $discount_amount; + } + + /** + * Set the SKU of this payment line. + * + * @param string|null $sku SKU. + */ + public function set_sku( ?string $sku ) : void { + $this->sku = $sku; + } + + /** + * Set image URL. + * + * @param string|null $image_url Image url. + */ + public function set_image_url( ?string $image_url ) : void { + $this->image_url = $image_url; + } + + /** + * Set product URL. + * + * @param string|null $product_url Product URL. + */ + public function set_product_url( ?string $product_url = null ) : void { + $this->product_url = $product_url; + } + + /** + * JSON serialize. + * + * @return mixed + */ + public function jsonSerialize() { + $object_builder = new ObjectBuilder(); + + $object_builder->set_optional( 'type', $this->type ); + $object_builder->set_optional( 'category', $this->category ); + $object_builder->set_required( 'name', $this->name ); + $object_builder->set_required( 'quantity', $this->quantity ); + $object_builder->set_required( 'unitPrice', $this->unit_price->jsonSerialize() ); + $object_builder->set_optional( 'discountAmount', null === $this->discount_amount ? null : $this->discount_amount->jsonSerialize() ); + $object_builder->set_optional( 'totalAmount', $this->total_amount->jsonSerialize() ); + $object_builder->set_required( 'vatRate', $this->vat_rate->format( 2, '.', '' ) ); + $object_builder->set_required( 'vatAmount', $this->vat_amount->jsonSerialize() ); + $object_builder->set_optional( 'sku', $this->sku ); + $object_builder->set_optional( 'imageUrl', $this->image_url ); + $object_builder->set_optional( 'productUrl', $this->product_url ); + + return $object_builder->jsonSerialize(); + } +} diff --git a/src/LineType.php b/src/LineType.php new file mode 100644 index 0000000..710b82c --- /dev/null +++ b/src/LineType.php @@ -0,0 +1,101 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay\Gateways\Mollie + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +use Pronamic\WordPress\Pay\Payments\PaymentLineType; + +/** + * Line type class + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + */ +class LineType { + /** + * Constant for 'digital' type. + * + * @var string + */ + const DIGITAL = 'digital'; + + /** + * Constant for 'discount' type. + * + * @var string + */ + const DISCOUNT = 'discount'; + + /** + * Constant for 'gift_card' type. + * + * @var string + */ + const GIFT_CARD = 'gift_card'; + + /** + * Constant for 'physical' type. + * + * @var string + */ + const PHYSICAL = 'physical'; + + /** + * Constant for 'shipping_fee' type. + * + * @var string + */ + const SHIPPING_FEE = 'shipping_fee'; + + /** + * Constant for 'store_credit' type. + * + * @var string + */ + const STORE_CREDIT = 'store_credit'; + + /** + * Constant for 'surcharge' type. + * + * @var string + */ + const SURCHARGE = 'surcharge'; + + /** + * Line type map. + * + * @var array + */ + private static $map = [ + PaymentLineType::DIGITAL => self::DIGITAL, + PaymentLineType::DISCOUNT => self::DISCOUNT, + PaymentLineType::FEE => self::SURCHARGE, + PaymentLineType::PHYSICAL => self::PHYSICAL, + PaymentLineType::SHIPPING => self::SHIPPING_FEE, + ]; + + /** + * Transform WordPress payment line type to Mollie line type. + * + * @since 4.3.0 + * @param string $payment_line_type WordPress payment line type to transform to Mollie line type. + * @return string|null + */ + public static function transform( $payment_line_type ) { + if ( ! is_scalar( $payment_line_type ) ) { + return null; + } + + if ( isset( self::$map[ $payment_line_type ] ) ) { + return self::$map[ $payment_line_type ]; + } + + return null; + } +} diff --git a/src/Lines.php b/src/Lines.php new file mode 100644 index 0000000..9e225d9 --- /dev/null +++ b/src/Lines.php @@ -0,0 +1,146 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay\Gateways\Mollie + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +use JsonSerializable; +use Pronamic\WordPress\Money\TaxedMoney; +use Pronamic\WordPress\Number\Number; +use Pronamic\WordPress\Pay\Payments\PaymentLines; + +/** + * Lines class + */ +class Lines implements JsonSerializable { + /** + * The lines. + * + * @var Line[] + */ + private array $lines = []; + + /** + * New line. + * + * @param string $name Description of the order line. + * @param int $quantity Quantity. + * @param Amount $unit_price Unit price. + * @param Amount $total_amount Total amount, including VAT and discounts. + * @param Number $vat_rate VAT rate. + * @param Amount $vat_amount Value-added tax amount. + */ + public function new_line( string $name, int $quantity, Amount $unit_price, Amount $total_amount, Number $vat_rate, Amount $vat_amount ) : Line { + $line = new Line( + $name, + $quantity, + $unit_price, + $total_amount, + $vat_rate, + $vat_amount + ); + + $this->lines[] = $line; + + return $line; + } + + /** + * JSON serialize. + * + * @return mixed + */ + public function jsonSerialize() { + $objects = array_map( + /** + * Get JSON for payment line. + * + * @param Line $line Payment line. + * @return object + */ + function( Line $line ) { + return $line->jsonSerialize(); + }, + $this->lines + ); + + return $objects; + } + + /** + * Create lines from WordPress Pay core payment lines. + * + * @param PaymentLines $payment_lines Payment lines. + * @return Lines + * @throws \InvalidArgumentException Throws exception on invalid arguments. + */ + public static function from_wp_payment_lines( PaymentLines $payment_lines ) : Lines { + $lines = new self(); + + foreach ( $payment_lines as $payment_line ) { + $total_amount = $payment_line->get_total_amount(); + + if ( ! $total_amount instanceof TaxedMoney ) { + throw new \InvalidArgumentException( 'Payment line requires tax information.' ); + } + + $unit_price = $payment_line->get_unit_price(); + + if ( null === $unit_price ) { + throw new \InvalidArgumentException( 'Payment line unit price is required.' ); + } + + $vat_amount = $payment_line->get_tax_amount(); + + if ( null === $vat_amount ) { + throw new \InvalidArgumentException( 'Payment line VAT amount is required.' ); + } + + $tax_percentage = $total_amount->get_tax_percentage(); + + if ( null === $tax_percentage ) { + throw new \InvalidArgumentException( 'Payment line VAT rate is required.' ); + } + + $name = $payment_line->get_name(); + + if ( null === $name ) { + throw new \InvalidArgumentException( 'Payment line name is required.' ); + } + + $quantity = $payment_line->get_quantity(); + + if ( null === $quantity ) { + throw new \InvalidArgumentException( 'Payment line quantity is required.' ); + } + + $line = $lines->new_line( + $name, + $quantity, + AmountTransformer::transform( $unit_price ), + AmountTransformer::transform( $total_amount ), + Number::from_mixed( $tax_percentage ), + AmountTransformer::transform( $vat_amount ), + ); + + $line->set_type( LineType::transform( $payment_line->get_type() ) ); + $line->set_category( $payment_line->get_product_category() ); + $line->set_sku( $payment_line->get_sku() ); + $line->set_image_url( $payment_line->get_image_url() ); + $line->set_product_url( $payment_line->get_product_url() ); + + // Discount amount. + $discount_amount = $payment_line->get_discount_amount(); + + $line->set_discount_amount( null === $discount_amount ? null : AmountTransformer::transform( $discount_amount ) ); + } + + return $lines; + } +} diff --git a/src/LocaleHelper.php b/src/LocaleHelper.php index 2412a30..1cb9d4f 100644 --- a/src/LocaleHelper.php +++ b/src/LocaleHelper.php @@ -11,14 +11,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Title: Mollie locale helper - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.0.9 - * @since 1.0.0 + * Locale helper class */ class LocaleHelper { /** diff --git a/src/Locales.php b/src/Locales.php index 3e9e59e..008b1ea 100644 --- a/src/Locales.php +++ b/src/Locales.php @@ -11,14 +11,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Title: Mollie locales - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.1.0 - * @since 1.0.0 + * Locales class */ class Locales { /** diff --git a/src/Mandate.php b/src/Mandate.php new file mode 100644 index 0000000..160eaad --- /dev/null +++ b/src/Mandate.php @@ -0,0 +1,43 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay\Gateways\Mollie + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +/** + * Mandate class + */ +class Mandate extends BaseResource { + /** + * Create mandate from JSON. + * + * @param object $json JSON object. + * @return self + * @throws \JsonSchema\Exception\ValidationException Throws JSON schema validation exception when JSON is invalid. + */ + public static function from_json( $json ) { + $validator = new \JsonSchema\Validator(); + + $validator->validate( + $json, + (object) [ + '$ref' => 'file://' . realpath( __DIR__ . '/../json-schemas/mandate.json' ), + ], + \JsonSchema\Constraints\Constraint::CHECK_MODE_EXCEPTIONS + ); + + $object_access = new ObjectAccess( $json ); + + $mandate = new Mandate( + $object_access->get_property( 'id' ) + ); + + return $mandate; + } +} diff --git a/src/Methods.php b/src/Methods.php index 95e44a4..dae1553 100644 --- a/src/Methods.php +++ b/src/Methods.php @@ -13,14 +13,7 @@ use Pronamic\WordPress\Pay\Core\PaymentMethods; /** - * Title: Mollie methods - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.0.9 - * @since 1.0.0 + * Methods class */ class Methods { /** @@ -86,6 +79,27 @@ class Methods { */ const GIROPAY = 'giropay'; + /** + * Constant for the Klarna - Pay Later method. + * + * @var string + */ + const KLARNA_PAY_LATER = 'klarnapaylater'; + + /** + * Constant for the Klarna - Pay Now method. + * + * @var string + */ + const KLARNA_PAY_NOW = 'klarnapaynow'; + + /** + * Constant for the Klarna - Slice It method. + * + * @var string + */ + const KLARNA_SLICE_IT = 'klarnasliceit'; + /** * Constant for the PayPal method. * @@ -150,6 +164,9 @@ class Methods { PaymentMethods::DIRECT_DEBIT_SOFORT => self::DIRECT_DEBIT, PaymentMethods::EPS => self::EPS, PaymentMethods::GIROPAY => self::GIROPAY, + PaymentMethods::KLARNA_PAY_LATER => self::KLARNA_PAY_LATER, + PaymentMethods::KLARNA_PAY_NOW => self::KLARNA_PAY_NOW, + PaymentMethods::KLARNA_PAY_OVER_TIME => self::KLARNA_SLICE_IT, PaymentMethods::PAYPAL => self::PAYPAL, PaymentMethods::PRZELEWY24 => self::PRZELEWY24, PaymentMethods::SOFORT => self::SOFORT, diff --git a/src/ObjectAccess.php b/src/ObjectAccess.php new file mode 100644 index 0000000..f2a8bba --- /dev/null +++ b/src/ObjectAccess.php @@ -0,0 +1,71 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay\Gateways\Mollie + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +/** + * Object access class + */ +class ObjectAccess { + /** + * Object. + * + * @var object Object. + */ + private $object; + + /** + * Construct object access. + * + * @param object $object Object. + */ + public function __construct( object $object ) { + $this->object = $object; + } + + /** + * Checks if the object has a property. + * + * @param string $property Property. + * @return bool True if the property exists, false if it doesn't exist. + */ + public function has_property( string $property ) { + return \property_exists( $this->object, $property ); + } + + /** + * Get property. + * + * @param string $property Property. + * @return mixed + * @throws \Exception Throws exception when property does not exists. + */ + public function get_property( string $property ) { + if ( ! \property_exists( $this->object, $property ) ) { + throw new \Exception( \sprintf( 'Object does not have `%s` property.', $property ) ); + } + + return $this->object->{$property}; + } + + /** + * Get optional. + * + * @param string $property Property. + * @return mixed + */ + public function get_optional( $property ) { + if ( ! \property_exists( $this->object, $property ) ) { + return null; + } + + return $this->object->{$property}; + } +} diff --git a/src/ObjectBuilder.php b/src/ObjectBuilder.php new file mode 100644 index 0000000..0b1d21c --- /dev/null +++ b/src/ObjectBuilder.php @@ -0,0 +1,72 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay\Gateways\Mollie + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +use JsonSerializable; + +/** + * Object builder class + */ +class ObjectBuilder implements JsonSerializable { + /** + * Data. + * + * @var mixed[] Data. + */ + private $data = []; + + /** + * Set optional value. + * + * @param string $key Key. + * @param mixed $value Value. + * @return void + */ + public function set_optional( string $key, $value ) { + if ( null === $value ) { + return; + } + + $this->set_value( $key, $value ); + } + + /** + * Set required value. + * + * @param string $key Key. + * @param mixed $value Value. + * @return void + */ + public function set_required( string $key, $value ) { + $this->set_value( $key, $value ); + } + + /** + * Set value. + * + * @param string $key Key. + * @param mixed $value Value. + * @return void + */ + private function set_value( string $key, $value ) { + $this->data[ $key ] = $value; + } + + /** + * JSON serialize. + * + * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed + */ + public function jsonSerialize() { + return (object) $this->data; + } +} diff --git a/src/Order.php b/src/Order.php new file mode 100644 index 0000000..f3a58f2 --- /dev/null +++ b/src/Order.php @@ -0,0 +1,104 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay\Gateways\Mollie + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +/** + * Order class + */ +class Order extends BaseResource { + /** + * Payments. + * + * @var Payment[]|null + */ + private ?array $payments; + + /** + * Status. + * + * @var string + */ + private string $status; + + /** + * Get embedded payments. + * + * @return Payment[]|null + */ + public function get_payments() : ?array { + return $this->payments; + } + + /** + * Set embedded payments. + * + * @param Payment[]|null $payments Payments. + */ + public function set_payments( ?array $payments ) : void { + $this->payments = $payments; + } + + /** + * Get status. + * + * @return string + */ + public function get_status() : string { + return $this->status; + } + + /** + * Create order from JSON. + * + * @link https://docs.mollie.com/reference/v2/orders-api/get-order + * @param object $json JSON object. + * @return Order + * @throws \JsonSchema\Exception\ValidationException Throws JSON schema validation exception when JSON is invalid. + */ + public static function from_json( $json ) { + $validator = new \JsonSchema\Validator(); + + $validator->validate( + $json, + (object) [ + '$ref' => 'file://' . realpath( __DIR__ . '/../json-schemas/order.json' ), + ], + \JsonSchema\Constraints\Constraint::CHECK_MODE_EXCEPTIONS + ); + + $object_access = new ObjectAccess( $json ); + + $order = new Order( $object_access->get_property( 'id' ) ); + + $order->status = $object_access->get_property( 'status' ); + + if ( property_exists( $json, '_embedded' ) ) { + if ( property_exists( $json->_embedded, 'payments' ) ) { + $payments = array_map( + /** + * Get JSON for payments. + * + * @param object $payment Payment. + * @return Payment + */ + function( object $payment ) { + return Payment::from_json( $payment ); + }, + $json->_embedded->payments + ); + + $order->set_payments( $payments ); + } + } + + return $order; + } +} diff --git a/src/OrderRequest.php b/src/OrderRequest.php new file mode 100644 index 0000000..5928dda --- /dev/null +++ b/src/OrderRequest.php @@ -0,0 +1,253 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +use DateTimeInterface; +use JsonSerializable; + +/** + * Order request class + */ +class OrderRequest implements JsonSerializable { + /** + * The total amount of the order, including VAT and discounts. This is the amount that + * will be charged to your customer. It has to match the sum of the lines totalAmount amounts. + * For example: {"currency":"EUR", "value":"100.00"} if the total order amount is €100.00. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var Amount + */ + private Amount $amount; + + /** + * The order number. For example, `16738`. We recommend that each order + * should have a unique order number. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var string + */ + private string $order_number; + + /** + * The lines in the order. Each line contains details such as a description of the item ordered, + * its price et cetera. All order lines must have the same currency as the order. You cannot + * mix currencies within a single order. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var Lines + */ + private Lines $lines; + + /** + * The billing person and address for the order. This field is not required if you + * make use of the PayPal Express Checkout button. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var Address|null + */ + public ?Address $billing_address = null; + + /** + * The shipping address for the order. This field is optional, but if it is provided, + * then the full name and address have to be in a valid format. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var Address|null + */ + public ?Address $shipping_address = null; + + /** + * 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. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var DateTimeInterface|null + */ + public ?DateTimeInterface $consumer_date_of_birth = null; + + /** + * 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. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var string|null + */ + public ?string $redirect_url = null; + + /** + * Set the webhook URL, where we will send order status changes to. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var string|null + */ + public ?string $webhook_url = null; + + /** + * Allows you to preset the language to be used in the hosted payment pages shown to + * the consumer. You can provide any `xx_XX` format ISO 15897 locale, but our hosted + * payment pages does not support all languages. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var string + */ + private string $locale; + + /** + * 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. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var string|array|null + */ + public $method; + + /** + * Any payment specific properties (for example, the `dueDate` for bank transfer payments) + * can be passed here. + * + * The payment property should be an object where the keys are the payment method specific + * parameters you want to pass. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order#payment-parameters + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @var array|null + */ + public ?array $payment = null; + + /** + * Provide any data you like in JSON notation, and 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 1kB of JSON. + * + * @link https://docs.mollie.com/reference/v2/orders-api/create-order + * @link https://en.wikipedia.org/wiki/Metadata + * @var object|string|null + */ + private $metadata; + + /** + * Create Mollie payment request object. + * + * @param Amount $amount The amount that you want to charge. + * @param string $order_number The order number. + * @param Lines $lines The lines in the order. + * @param string $locale Locale. + */ + public function __construct( Amount $amount, string $order_number, Lines $lines, string $locale ) { + $this->amount = $amount; + $this->order_number = $order_number; + $this->lines = $lines; + $this->locale = $locale; + } + + /** + * Set billing address. + * + * @param Address|null $billing_address Billing address. + */ + public function set_billing_address( ?Address $billing_address ) : void { + $this->billing_address = $billing_address; + } + + /** + * Set shipping address. + * + * @param Address|null $shipping_address Shipping address. + */ + public function set_shipping_address( ?Address $shipping_address ) : void { + $this->shipping_address = $shipping_address; + } + + /** + * Set consumer date of birth. + * + * @param DateTimeInterface|null $consumer_date_of_birth Consumer date of birth. + */ + public function set_consumer_date_of_birth( ?DateTimeInterface $consumer_date_of_birth ) : void { + $this->consumer_date_of_birth = $consumer_date_of_birth; + } + + /** + * Set redirect URL. + * + * @param string|null $redirect_url Redirect URL. + */ + public function set_redirect_url( ?string $redirect_url ) : void { + $this->redirect_url = $redirect_url; + } + + /** + * Set webhook URL. + * + * @param string|null $webhook_url Webhook URL. + */ + public function set_webhook_url( ?string $webhook_url ) : void { + $this->webhook_url = $webhook_url; + } + + /** + * Set method. + * + * @param array|string|null $method Method. + */ + public function set_method( $method ) : void { + $this->method = $method; + } + + /** + * Set payment. + * + * @param array|null $payment Payment specific parameters. + * @link https://docs.mollie.com/reference/v2/orders-api/create-order#payment-specific-parameters + */ + public function set_payment( ?array $payment ) : void { + $this->payment = $payment; + } + + /** + * Set metadata. + * + * @link https://docs.mollie.com/reference/v2/payments-api/create-payment + * @link https://en.wikipedia.org/wiki/Metadata + * @param mixed $metadata Metadata. + * @return void + */ + public function set_metadata( $metadata = null ) : void { + $this->metadata = $metadata; + } + + /** + * JSON serialize. + * + * @return mixed + */ + public function jsonSerialize() { + $object_builder = new ObjectBuilder(); + + $object_builder->set_required( 'amount', $this->amount->jsonSerialize() ); + $object_builder->set_required( 'orderNumber', $this->order_number ); + $object_builder->set_required( 'lines', $this->lines->jsonSerialize() ); + $object_builder->set_optional( 'billingAddress', null === $this->billing_address ? null : $this->billing_address->jsonSerialize() ); + $object_builder->set_optional( 'shippingAddress', null === $this->shipping_address ? null : $this->shipping_address->jsonSerialize() ); + $object_builder->set_optional( 'consumerDateOfBirth', null === $this->consumer_date_of_birth ? null : $this->consumer_date_of_birth->format( 'Y-m-d' ) ); + $object_builder->set_optional( 'redirectUrl', $this->redirect_url ); + $object_builder->set_optional( 'webhookUrl', $this->webhook_url ); + $object_builder->set_required( 'locale', $this->locale ); + $object_builder->set_optional( 'method', $this->method ); + $object_builder->set_optional( 'payment', $this->payment ); + $object_builder->set_optional( 'metadata', $this->metadata ); + + return $object_builder->jsonSerialize(); + } +} diff --git a/src/Payment.php b/src/Payment.php index e5ef668..c1d5bd4 100644 --- a/src/Payment.php +++ b/src/Payment.php @@ -11,13 +11,10 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; use DateTimeInterface; +use DateTimeImmutable; /** - * Payment - * - * @author Remco Tolsma - * @version 2.2.2 - * @since 2.1.0 + * Payment class */ class Payment extends BaseResource { /** @@ -111,13 +108,6 @@ class Payment extends BaseResource { */ private $metadata; - /** - * The customer’s locale, either forced on creation by specifying the `locale` parameter, or detected by us during checkout. Will be a full locale, for example `nl_NL`. - * - * @var string - */ - private $locale; - /** * 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. @@ -160,7 +150,7 @@ class Payment extends BaseResource { * @param string $description Description. * @param string|null $redirect_url Redirect URL. * @param string|null $method Method. - * @param string $metadata Metadata. + * @param mixed $metadata Metadata. * @param string $profile_id Profile ID. * @param string $sequence_type Sequence type. * @param object $links Links. @@ -181,6 +171,24 @@ public function __construct( $id, $mode, DateTimeInterface $created_at, $status, $this->links = $links; } + /** + * Get mode. + * + * @return string + */ + public function get_mode() { + return $this->mode; + } + + /** + * Get created at. + * + * @return DateTimeInterface + */ + public function get_created_at() { + return $this->created_at; + } + /** * Get status. * @@ -191,49 +199,57 @@ public function get_status() { } /** - * Get method. + * Get amount. * - * @return string|null + * @return Amount */ - public function get_method() { - return $this->method; + public function get_amount() { + return $this->amount; } /** - * Get sequence type. + * Get description. * * @return string */ - public function get_sequence_type() { - return $this->sequence_type; + public function get_description() { + return $this->description; } /** - * Get profile ID. + * Get redirect URL. * - * @return string + * @return string|null */ - public function get_profile_id() { - return $this->profile_id; + public function get_redirect_url() { + return $this->redirect_url; } /** - * Get locale. + * Get method. + * + * @return string|null + */ + public function get_method() { + return $this->method; + } + + /** + * Get sequence type. * * @return string */ - public function get_locale() { - return $this->locale; + public function get_sequence_type() { + return $this->sequence_type; } /** - * Set locale. + * Get profile ID. * - * @param string $locale Locale. - * @return void + * @return string */ - public function set_locale( $locale ) { - $this->locale = $locale; + public function get_profile_id() { + return $this->profile_id; } /** @@ -379,6 +395,16 @@ public function set_links( $links ) { $this->links = $links; } + /** + * Get metadata. + * + * @return mixed + */ + public function get_metadata() { + return $this->metadata; + } + + /** * Create payment from JSON. * @@ -398,56 +424,42 @@ public static function from_json( $json ) { \JsonSchema\Constraints\Constraint::CHECK_MODE_EXCEPTIONS ); - // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object. + $object_access = new ObjectAccess( $json ); + $payment = new Payment( - $json->id, - $json->mode, - new \DateTimeImmutable( $json->createdAt ), - $json->status, - Amount::from_json( $json->amount ), - $json->description, - $json->redirectUrl, - $json->method, - $json->metadata, - $json->profileId, - $json->sequenceType, - $json->_links + $object_access->get_property( 'id' ), + $object_access->get_property( 'mode' ), + new DateTimeImmutable( $object_access->get_property( 'createdAt' ) ), + $object_access->get_property( 'status' ), + Amount::from_json( $object_access->get_property( 'amount' ) ), + $object_access->get_property( 'description' ), + $object_access->get_property( 'redirectUrl' ), + $object_access->get_property( 'method' ), + $object_access->get_property( 'metadata' ), + $object_access->get_property( 'profileId' ), + $object_access->get_property( 'sequenceType' ), + $object_access->get_property( '_links' ), ); - if ( \property_exists( $json, 'expiresAt' ) ) { - $payment->set_expires_at( new \DateTimeImmutable( $json->expiresAt ) ); + if ( $object_access->has_property( 'expiresAt' ) ) { + $payment->set_expires_at( new DateTimeImmutable( $object_access->get_property( 'expiresAt' ) ) ); } - if ( \property_exists( $json, 'locale' ) ) { - $payment->set_locale( $json->locale ); - } + $payment->set_customer_id( $object_access->get_optional( 'customerId' ) ); + $payment->set_mandate_id( $object_access->get_optional( 'mandateId' ) ); - if ( \property_exists( $json, 'customerId' ) ) { - $payment->set_customer_id( $json->customerId ); + if ( $object_access->has_property( 'details' ) ) { + $payment->set_details( PaymentDetails::from_json( (string) $payment->get_method(), $object_access->get_property( 'details' ) ) ); } - if ( \property_exists( $json, 'mandateId' ) ) { - $payment->set_mandate_id( $json->mandateId ); + if ( $object_access->has_property( 'amountRefunded' ) ) { + $payment->set_amount_refunded( Amount::from_json( $object_access->get_property( 'amountRefunded' ) ) ); } - if ( \property_exists( $json, 'details' ) ) { - $payment->set_details( PaymentDetails::from_json( (string) $payment->get_method(), $json->details ) ); + if ( $object_access->has_property( 'amountChargedBack' ) ) { + $payment->set_amount_charged_back( Amount::from_json( $object_access->get_property( 'amountChargedBack' ) ) ); } - if ( \property_exists( $json, 'amountRefunded' ) ) { - $refunded_amount = Amount::from_json( $json->amountRefunded ); - - $payment->set_amount_refunded( $refunded_amount ); - } - - if ( \property_exists( $json, 'amountChargedBack' ) ) { - $charged_back_amount = Amount::from_json( $json->amountChargedBack ); - - $payment->set_amount_charged_back( $charged_back_amount ); - } - - // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object. - return $payment; } } diff --git a/src/PaymentDetails.php b/src/PaymentDetails.php index 003425f..82da0d9 100644 --- a/src/PaymentDetails.php +++ b/src/PaymentDetails.php @@ -11,11 +11,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Payment Details - * - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 + * Payment details class */ class PaymentDetails { /** diff --git a/src/PaymentRequest.php b/src/PaymentRequest.php index b57bdd3..cc933aa 100644 --- a/src/PaymentRequest.php +++ b/src/PaymentRequest.php @@ -10,17 +10,13 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; +use DateTimeInterface; +use JsonSerializable; + /** - * Title: Mollie payment request - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.1.4 - * @since 1.0.0 + * Payment request class */ -class PaymentRequest { +class PaymentRequest implements JsonSerializable { /** * The amount in EURO that you want to charge, e.g. `{"currency":"EUR", "value":"100.00"}` * if you would want to charge € 100,00. @@ -44,6 +40,9 @@ class PaymentRequest { * 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. * + * The parameter can be omitted for recurring payments (sequenceType: `recurring`) + * and for Apple Pay payments with an `applePayPaymentToken`. + * * @link https://www.mollie.com/nl/docs/reference/payments/create * @var string|null */ @@ -117,7 +116,7 @@ class PaymentRequest { * is tomorrow and the maximum date is 100 days after tomorrow. * * @link https://docs.mollie.com/reference/v2/payments-api/create-payment - * @var \DateTimeInterface|null + * @var DateTimeInterface|null */ private $due_date; @@ -206,7 +205,7 @@ public function set_method( $method ) { /** * Get due date. * - * @return null|\DateTimeInterface + * @return null|DateTimeInterface */ public function get_due_date() { return $this->due_date; @@ -215,7 +214,7 @@ public function get_due_date() { /** * Set due date. * - * @param null|\DateTimeInterface $due_date Due date. + * @param null|DateTimeInterface $due_date Due date. * @return void */ public function set_due_date( $due_date ) { @@ -317,41 +316,55 @@ public function set_metadata( $metadata = null ) { } /** - * Get array of this Mollie payment request object. + * JSON serialize. * - * @return array + * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed */ - public function get_array() { + public function jsonSerialize() { + $object_builder = new ObjectBuilder(); + + // General. + $object_builder->set_required( 'amount', $this->amount->jsonSerialize() ); + $object_builder->set_required( 'description', $this->description ); + + /** + * The `redirectUrl` is documented as `required` but is not always required: + * + * > The parameter can be omitted for recurring payments (sequenceType: `recurring`) + * > and for Apple Pay payments with an applePayPaymentToken. + * + * @link https://docs.mollie.com/reference/v2/payments-api/create-payment + */ + $object_builder->set_optional( 'redirectUrl', $this->redirect_url ); + + $object_builder->set_optional( 'webhookUrl', $this->webhook_url ); + $object_builder->set_optional( 'locale', $this->locale ); + $object_builder->set_optional( 'method', $this->method ); + $object_builder->set_optional( 'metadata', $this->metadata ); + + // Parameters for recurring payments. + $object_builder->set_optional( 'sequenceType', $this->sequence_type ); + $object_builder->set_optional( 'customerId', $this->customer_id ); + $object_builder->set_optional( 'mandateId', $this->mandate_id ); + + // Payment method-specific parameters. + $object_builder->set_optional( 'billingEmail', $this->billing_email ); + // Due date. $due_date = $this->get_due_date(); if ( null !== $due_date ) { - $due_date = $due_date->format( 'Y-m-d' ); + $object_builder->set_optional( 'dueDate', $due_date->format( 'Y-m-d' ) ); } - $array = [ - 'amount' => $this->amount->get_json(), - 'description' => $this->description, - 'method' => $this->method, - 'redirectUrl' => $this->redirect_url, - 'metadata' => $this->metadata, - 'locale' => $this->locale, - 'webhookUrl' => $this->webhook_url, - 'consumerName' => $this->consumer_name, - 'consumerAccount' => $this->consumer_account, - 'issuer' => $this->issuer, - 'billingEmail' => $this->billing_email, - 'dueDate' => $due_date, - 'sequenceType' => $this->sequence_type, - 'customerId' => $this->customer_id, - 'mandateId' => $this->mandate_id, - ]; - - /* - * Array filter will remove values NULL, FALSE and empty strings ('') - */ - $array = array_filter( $array ); + // IDeal. + $object_builder->set_optional( 'issuer', $this->issuer ); + + // SEPA Direct Debit. + $object_builder->set_optional( 'consumerName', $this->consumer_name ); + $object_builder->set_optional( 'consumerAccount', $this->consumer_account ); - return $array; + return $object_builder->jsonSerialize(); } } diff --git a/src/Profile.php b/src/Profile.php index fb4da77..60e479d 100644 --- a/src/Profile.php +++ b/src/Profile.php @@ -11,12 +11,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Mollie profile. - * - * @link https://docs.mollie.com/reference/v2/profiles-api/create-profile - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 + * Profile class */ class Profile { /** @@ -132,21 +127,12 @@ public function set_email( $email ) { public static function from_object( $object ) { $profile = new self(); - if ( property_exists( $object, 'id' ) ) { - $profile->set_id( $object->id ); - } - - if ( property_exists( $object, 'mode' ) ) { - $profile->set_mode( $object->mode ); - } - - if ( property_exists( $object, 'name' ) ) { - $profile->set_name( $object->name ); - } + $object_access = new ObjectAccess( $object ); - if ( property_exists( $object, 'email' ) ) { - $profile->set_email( $object->email ); - } + $profile->set_id( $object_access->get_optional( 'id' ) ); + $profile->set_mode( $object_access->get_optional( 'mode' ) ); + $profile->set_name( $object_access->get_optional( 'name' ) ); + $profile->set_email( $object_access->get_optional( 'email' ) ); return $profile; } diff --git a/src/ProfileDataStore.php b/src/ProfileDataStore.php index 39f6707..818736f 100644 --- a/src/ProfileDataStore.php +++ b/src/ProfileDataStore.php @@ -11,14 +11,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Title: Mollie profile data store - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 + * Profile data store class */ class ProfileDataStore { /** diff --git a/src/Refund.php b/src/Refund.php index a136315..71a2b52 100644 --- a/src/Refund.php +++ b/src/Refund.php @@ -13,11 +13,7 @@ use DateTimeInterface; /** - * Refund - * - * @author Reüel van der Steege - * @version 2.3.0 - * @since 2.3.0 + * Refund class */ class Refund extends BaseResource { /** @@ -35,14 +31,6 @@ class Refund extends BaseResource { */ private $description; - /** - * The optional metadata you provided upon refund creation. Metadata can for - * example be used to link an bookkeeping ID to a refund. - * - * @var mixed - */ - private $metadata; - /** * Since refunds may not be instant for certain payment methods, * the refund carries a status field. @@ -104,15 +92,6 @@ public function get_description() { return $this->description; } - /** - * Get metadata. - * - * @return mixed - */ - public function get_metadata() { - return $this->metadata; - } - /** * Get status. * @@ -159,19 +138,17 @@ public static function from_json( $json ) { \JsonSchema\Constraints\Constraint::CHECK_MODE_EXCEPTIONS ); - // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object. + $object_access = new ObjectAccess( $json ); $refund = new Refund( - $json->id, - Amount::from_json( $json->amount ), - $json->description, - $json->status, - $json->paymentId, - new \DateTimeImmutable( $json->createdAt ) + $object_access->get_property( 'id' ), + Amount::from_json( $object_access->get_property( 'amount' ) ), + $object_access->get_property( 'description' ), + $object_access->get_property( 'status' ), + $object_access->get_property( 'paymentId' ), + new \DateTimeImmutable( $object_access->get_property( 'createdAt' ) ) ); - // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - return $refund; } } diff --git a/src/RefundRequest.php b/src/RefundRequest.php index 2fbeabd..17f5c0b 100644 --- a/src/RefundRequest.php +++ b/src/RefundRequest.php @@ -10,17 +10,12 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; +use JsonSerializable; + /** - * Title: Mollie refund request - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Reüel van der Steege - * @version 2.3.0 - * @since 2.3.0 + * Refund request class */ -class RefundRequest { +class RefundRequest implements JsonSerializable { /** * The amount to refund. For some payments, it can be up to €25.00 more * than the original transaction amount. @@ -54,7 +49,7 @@ class RefundRequest { * Construct Mollie refund request object. * * @param Amount $amount The amount that you want to refund. - * @retrun void + * @return void */ public function __construct( $amount ) { $this->amount = $amount; @@ -103,22 +98,18 @@ public function set_metadata( $metadata = null ) { } /** - * Get array of this Mollie refund request object. + * JSON serialize. * - * @return array + * @link https://www.php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed */ - public function get_array() { - $array = [ - 'amount' => $this->amount->get_json(), - 'description' => $this->description, - 'metadata' => $this->metadata, - ]; + public function jsonSerialize() { + $object_builder = new ObjectBuilder(); - /* - * Array filter will remove values NULL, FALSE and empty strings ('') - */ - $array = array_filter( $array ); + $object_builder->set_required( 'amount', $this->amount->jsonSerialize() ); + $object_builder->set_optional( 'description', $this->description ); + $object_builder->set_optional( 'metadata', $this->metadata ); - return $array; + return $object_builder->jsonSerialize(); } } diff --git a/src/ResourceType.php b/src/ResourceType.php new file mode 100644 index 0000000..a296391 --- /dev/null +++ b/src/ResourceType.php @@ -0,0 +1,30 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +/** + * Resource type class + */ +class ResourceType { + /** + * Constant for payments. + * + * @var string + */ + const PAYMENTS = 'payments'; + + /** + * Constant for orders. + * + * @var string + */ + const ORDERS = 'orders'; +} diff --git a/src/Sequence.php b/src/Sequence.php index 2d5f340..17dc2dd 100644 --- a/src/Sequence.php +++ b/src/Sequence.php @@ -11,14 +11,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; /** - * Title: Mollie sequence - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic - * - * @author Reüel van der Steege - * @version 2.1.0 - * @since 1.1.9 + * Sequence class */ class Sequence { /** diff --git a/src/Shipment.php b/src/Shipment.php new file mode 100644 index 0000000..ad589e8 --- /dev/null +++ b/src/Shipment.php @@ -0,0 +1,42 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay\Gateways\Mollie + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +/** + * Shipment class + */ +class Shipment extends BaseResource { + /** + * Create shipment from JSON. + * + * @link https://docs.mollie.com/reference/v2/orders-api/get-shipment + * @param object $json JSON object. + * @return Shipment + * @throws \JsonSchema\Exception\ValidationException Throws JSON schema validation exception when JSON is invalid. + */ + public static function from_json( $json ) { + $validator = new \JsonSchema\Validator(); + + $validator->validate( + $json, + (object) [ + '$ref' => 'file://' . realpath( __DIR__ . '/../json-schemas/shipment.json' ), + ], + \JsonSchema\Constraints\Constraint::CHECK_MODE_EXCEPTIONS + ); + + $object_access = new ObjectAccess( $json ); + + $shipment = new Shipment( $object_access->get_property( 'id' ) ); + + return $shipment; + } +} diff --git a/src/Statuses.php b/src/Statuses.php index 2d2c08f..4520cc7 100644 --- a/src/Statuses.php +++ b/src/Statuses.php @@ -13,16 +13,9 @@ use Pronamic\WordPress\Pay\Payments\PaymentStatus; /** - * Title: Mollie statuses constants - * Description: - * Copyright: 2005-2022 Pronamic - * Company: Pronamic + * Statuses class * * @link https://docs.mollie.com/payments/status-changes - * - * @author Remco Tolsma - * @version 2.1.0 - * @since 1.0.0 */ class Statuses { /** @@ -89,6 +82,8 @@ public static function transform( $status ) { return PaymentStatus::OPEN; case self::CANCELED: return PaymentStatus::CANCELLED; + case self::AUTHORIZED: + return PaymentStatus::AUTHORIZED; case self::PAID: return PaymentStatus::SUCCESS; case self::EXPIRED: diff --git a/src/WebhookController.php b/src/WebhookController.php index 876362b..a26e859 100644 --- a/src/WebhookController.php +++ b/src/WebhookController.php @@ -15,13 +15,9 @@ use WP_REST_Response; /** - * Webhook controller + * Webhook controller class * * @link https://docs.mollie.com/guides/webhooks - * - * @author Remco Tolsma - * @version 2.1.0 - * @since 2.1.0 */ class WebhookController { /** @@ -83,6 +79,50 @@ public function rest_api_init() { 'permission_callback' => '__return_true', ] ); + + \register_rest_route( + Integration::REST_ROUTE_NAMESPACE, + '/payments/webhook/(?P\d+)', + [ + 'methods' => 'POST', + 'callback' => [ $this, 'rest_api_mollie_webhook_payment' ], + 'args' => [ + 'payment_id' => [ + 'description' => \__( 'Payment ID.', 'pronamic_ideal' ), + 'type' => 'string', + 'required' => true, + ], + 'id' => [ + 'description' => \__( 'Mollie transaction ID.', 'pronamic_ideal' ), + 'type' => 'string', + 'required' => true, + ], + ], + 'permission_callback' => '__return_true', + ] + ); + + \register_rest_route( + Integration::REST_ROUTE_NAMESPACE, + '/orders/webhook/(?P\d+)', + [ + 'methods' => 'POST', + 'callback' => [ $this, 'rest_api_mollie_webhook_order' ], + 'args' => [ + 'payment_id' => [ + 'description' => \__( 'Payment ID.', 'pronamic_ideal' ), + 'type' => 'string', + 'required' => true, + ], + 'id' => [ + 'description' => \__( 'Mollie order ID.', 'pronamic_ideal' ), + 'type' => 'string', + 'required' => true, + ], + ], + 'permission_callback' => '__return_true', + ] + ); } /** @@ -108,7 +148,7 @@ public function rest_api_mollie_webhook( WP_REST_Request $request ) { } /** - * REST API Mollie webhook handler. + * REST API Mollie payment webhook handler. * * @param WP_REST_Request $request Request. * @return object @@ -154,12 +194,68 @@ public function rest_api_mollie_webhook_payment( WP_REST_Request $request ) { } // Add note. - $note = \sprintf( - /* translators: %s: payment provider name */ - \__( 'Webhook requested by %s.', 'pronamic_ideal' ), - \__( 'Mollie', 'pronamic_ideal' ) + $note = \__( 'Payment webhook requested by Mollie.', 'pronamic_ideal' ); + + $payment->add_note( $note ); + + // Log webhook request. + \do_action( 'pronamic_pay_webhook_log_payment', $payment ); + + // Update payment. + Plugin::update_payment( $payment, false ); + + return $response; + } + + /** + * REST API Mollie order webhook handler. + * + * @param WP_REST_Request $request Request. + * @return object + */ + public function rest_api_mollie_webhook_order( WP_REST_Request $request ) { + $id = $request->get_param( 'id' ); + + /** + * Result. + * + * @link https://developer.wordpress.org/reference/functions/wp_send_json_success/ + */ + $response = new WP_REST_Response( + [ + 'success' => true, + 'id' => $id, + ] ); + $response->add_link( 'self', rest_url( $request->get_route() ) ); + + /** + * Payment. + */ + $payment_id = $request->get_param( 'payment_id' ); + + if ( empty( $payment_id ) ) { + return $response; + } + + $payment = \get_pronamic_payment( $payment_id ); + + if ( null === $payment ) { + /** + * How to handle unknown IDs? + * + * To not leak any information to malicious third parties, it is recommended + * to return a 200 OK response even if the ID is not known to your system. + * + * @link https://docs.mollie.com/guides/webhooks#how-to-handle-unknown-ids + */ + return $response; + } + + // Add note. + $note = \__( 'Order webhook requested by Mollie.', 'pronamic_ideal' ); + $payment->add_note( $note ); // Log webhook request. diff --git a/tests/http/api-mollie-com-v2-methods-first.http b/tests/http/api-mollie-com-v2-methods-first.http index 45ddf8e..bad756f 100644 --- a/tests/http/api-mollie-com-v2-methods-first.http +++ b/tests/http/api-mollie-com-v2-methods-first.http @@ -1,10 +1,10 @@ HTTP/1.1 200 OK Server: nginx -Date: Wed, 20 Nov 2019 14:14:28 GMT +Date: Tue, 14 Jun 2022 09:58:44 GMT Content-Type: application/hal+json -Content-Length: 3872 -Connection: close -X-Content-Type-Options: nosniff +Content-Length: 4561 +X-Robots-Tag: noindex +X-XSS-Protection: 1; mode=block Strict-Transport-Security: max-age=31536000; includeSubDomains; preload -{"_embedded":{"methods":[{"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"},"_links":{"self":{"href":"https://api.mollie.com/v2/methods/ideal","type":"application/hal+json"}}},{"resource":"method","id":"creditcard","description":"Credit card","minimumAmount":{"value":"0.00","currency":"EUR"},"maximumAmount":{"value":"2000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/creditcard.png","size2x":"https://www.mollie.com/external/icons/payment-methods/creditcard%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/creditcard.svg"},"_links":{"self":{"href":"https://api.mollie.com/v2/methods/creditcard","type":"application/hal+json"}}},{"resource":"method","id":"bancontact","description":"Bancontact","minimumAmount":{"value":"0.02","currency":"EUR"},"maximumAmount":{"value":"50000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/bancontact.png","size2x":"https://www.mollie.com/external/icons/payment-methods/bancontact%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/bancontact.svg"},"_links":{"self":{"href":"https://api.mollie.com/v2/methods/bancontact","type":"application/hal+json"}}},{"resource":"method","id":"sofort","description":"SOFORT Banking","minimumAmount":{"value":"0.10","currency":"EUR"},"maximumAmount":{"value":"50000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/sofort.png","size2x":"https://www.mollie.com/external/icons/payment-methods/sofort%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/sofort.svg"},"_links":{"self":{"href":"https://api.mollie.com/v2/methods/sofort","type":"application/hal+json"}}},{"resource":"method","id":"kbc","description":"KBC/CBC Payment Button","minimumAmount":{"value":"0.01","currency":"EUR"},"maximumAmount":{"value":"50000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/kbc.png","size2x":"https://www.mollie.com/external/icons/payment-methods/kbc%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/kbc.svg"},"_links":{"self":{"href":"https://api.mollie.com/v2/methods/kbc","type":"application/hal+json"}}},{"resource":"method","id":"belfius","description":"Belfius Pay Button","minimumAmount":{"value":"0.01","currency":"EUR"},"maximumAmount":{"value":"50000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/belfius.png","size2x":"https://www.mollie.com/external/icons/payment-methods/belfius%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/belfius.svg"},"_links":{"self":{"href":"https://api.mollie.com/v2/methods/belfius","type":"application/hal+json"}}},{"resource":"method","id":"inghomepay","description":"ING Home\'Pay","minimumAmount":{"value":"0.01","currency":"EUR"},"maximumAmount":{"value":"50000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/inghomepay.png","size2x":"https://www.mollie.com/external/icons/payment-methods/inghomepay%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/inghomepay.svg"},"_links":{"self":{"href":"https://api.mollie.com/v2/methods/inghomepay","type":"application/hal+json"}}}]},"count":7,"_links":{"documentation":{"href":"https://docs.mollie.com/reference/v2/methods-api/list-methods","type":"text/html"},"self":{"href":"https://api.mollie.com/v2/methods?sequenceType=first","type":"application/hal+json"}}} +{"_embedded":{"methods":[{"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":"pending-boarding","_links":{"self":{"href":"https://api.mollie.com/v2/methods/ideal","type":"application/hal+json"}}},{"resource":"method","id":"creditcard","description":"Credit card","minimumAmount":{"value":"0.00","currency":"EUR"},"maximumAmount":{"value":"2000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/creditcard.png","size2x":"https://www.mollie.com/external/icons/payment-methods/creditcard%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/creditcard.svg"},"status":"pending-boarding","_links":{"self":{"href":"https://api.mollie.com/v2/methods/creditcard","type":"application/hal+json"}}},{"resource":"method","id":"sofort","description":"SOFORT Banking","minimumAmount":{"value":"0.10","currency":"EUR"},"maximumAmount":{"value":"50000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/sofort.png","size2x":"https://www.mollie.com/external/icons/payment-methods/sofort%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/sofort.svg"},"status":"pending-boarding","_links":{"self":{"href":"https://api.mollie.com/v2/methods/sofort","type":"application/hal+json"}}},{"resource":"method","id":"bancontact","description":"Bancontact","minimumAmount":{"value":"0.02","currency":"EUR"},"maximumAmount":{"value":"50000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/bancontact.png","size2x":"https://www.mollie.com/external/icons/payment-methods/bancontact%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/bancontact.svg"},"status":"pending-boarding","_links":{"self":{"href":"https://api.mollie.com/v2/methods/bancontact","type":"application/hal+json"}}},{"resource":"method","id":"eps","description":"eps","minimumAmount":{"value":"1.00","currency":"EUR"},"maximumAmount":{"value":"50000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/eps.png","size2x":"https://www.mollie.com/external/icons/payment-methods/eps%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/eps.svg"},"status":"pending-boarding","_links":{"self":{"href":"https://api.mollie.com/v2/methods/eps","type":"application/hal+json"}}},{"resource":"method","id":"giropay","description":"giropay","minimumAmount":{"value":"1.00","currency":"EUR"},"maximumAmount":{"value":"10000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/giropay.png","size2x":"https://www.mollie.com/external/icons/payment-methods/giropay%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/giropay.svg"},"status":"pending-boarding","_links":{"self":{"href":"https://api.mollie.com/v2/methods/giropay","type":"application/hal+json"}}},{"resource":"method","id":"kbc","description":"KBC/CBC Payment Button","minimumAmount":{"value":"0.01","currency":"EUR"},"maximumAmount":{"value":"50000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/kbc.png","size2x":"https://www.mollie.com/external/icons/payment-methods/kbc%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/kbc.svg"},"status":"pending-boarding","_links":{"self":{"href":"https://api.mollie.com/v2/methods/kbc","type":"application/hal+json"}}},{"resource":"method","id":"belfius","description":"Belfius Pay Button","minimumAmount":{"value":"0.01","currency":"EUR"},"maximumAmount":{"value":"50000.00","currency":"EUR"},"image":{"size1x":"https://www.mollie.com/external/icons/payment-methods/belfius.png","size2x":"https://www.mollie.com/external/icons/payment-methods/belfius%402x.png","svg":"https://www.mollie.com/external/icons/payment-methods/belfius.svg"},"status":"pending-boarding","_links":{"self":{"href":"https://api.mollie.com/v2/methods/belfius","type":"application/hal+json"}}}]},"count":8,"_links":{"documentation":{"href":"https://docs.mollie.com/reference/v2/methods-api/list-methods","type":"text/html"},"self":{"href":"https://api.mollie.com/v2/methods?sequenceType=first","type":"application/hal+json"}}} \ No newline at end of file diff --git a/tests/http/api-mollie-com-v2-methods-ideal.http b/tests/http/api-mollie-com-v2-methods-ideal.http new file mode 100644 index 0000000..88c0a2e --- /dev/null +++ b/tests/http/api-mollie-com-v2-methods-ideal.http @@ -0,0 +1,10 @@ +HTTP/1.1 200 OK +Server: nginx +Date: Tue, 14 Jun 2022 10:00:07 GMT +Content-Type: application/hal+json +Content-Length: 4277 +X-Robots-Tag: noindex +X-XSS-Protection: 1; mode=block +Strict-Transport-Security: max-age=31536000; includeSubDomains; preload + +{"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":"pending-boarding","issuers":[{"resource":"issuer","id":"ideal_ABNANL2A","name":"ABN AMRO","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/ABNANL2A.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/ABNANL2A%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/ABNANL2A.svg"}},{"resource":"issuer","id":"ideal_INGBNL2A","name":"ING","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/INGBNL2A.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/INGBNL2A%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/INGBNL2A.svg"}},{"resource":"issuer","id":"ideal_RABONL2U","name":"Rabobank","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/RABONL2U.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/RABONL2U%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/RABONL2U.svg"}},{"resource":"issuer","id":"ideal_ASNBNL21","name":"ASN Bank","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/ASNBNL21.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/ASNBNL21%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/ASNBNL21.svg"}},{"resource":"issuer","id":"ideal_BUNQNL2A","name":"bunq","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/BUNQNL2A.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/BUNQNL2A%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/BUNQNL2A.svg"}},{"resource":"issuer","id":"ideal_HANDNL2A","name":"Handelsbanken","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/HANDNL2A.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/HANDNL2A%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/HANDNL2A.svg"}},{"resource":"issuer","id":"ideal_KNABNL2H","name":"Knab","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/KNABNL2H.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/KNABNL2H%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/KNABNL2H.svg"}},{"resource":"issuer","id":"ideal_RBRBNL21","name":"Regiobank","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/RBRBNL21.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/RBRBNL21%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/RBRBNL21.svg"}},{"resource":"issuer","id":"ideal_REVOLT21","name":"Revolut","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/REVOLT21.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/REVOLT21%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/REVOLT21.svg"}},{"resource":"issuer","id":"ideal_SNSBNL2A","name":"SNS Bank","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/SNSBNL2A.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/SNSBNL2A%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/SNSBNL2A.svg"}},{"resource":"issuer","id":"ideal_TRIONL2U","name":"Triodos","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/TRIONL2U.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/TRIONL2U%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/TRIONL2U.svg"}},{"resource":"issuer","id":"ideal_FVLBNL22","name":"Van Lanschot","image":{"size1x":"https://www.mollie.com/external/icons/ideal-issuers/FVLBNL22.png","size2x":"https://www.mollie.com/external/icons/ideal-issuers/FVLBNL22%402x.png","svg":"https://www.mollie.com/external/icons/ideal-issuers/FVLBNL22.svg"}}],"_links":{"self":{"href":"https://api.mollie.com/v2/methods/ideal?include=issuers","type":"application/hal+json"},"documentation":{"href":"https://docs.mollie.com/reference/v2/methods-api/get-method","type":"text/html"}}} \ No newline at end of file diff --git a/tests/src/AmountTest.php b/tests/src/AmountTest.php index 546e5be..19ab1f6 100644 --- a/tests/src/AmountTest.php +++ b/tests/src/AmountTest.php @@ -10,7 +10,7 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; -use InvalidArgumentException; +use Exception; /** * Title: Mollie amount tests @@ -33,8 +33,6 @@ public function test_setters_and_getters() { $this->assertEquals( 'EUR', $amount->get_currency() ); $this->assertEquals( '100.00', $amount->get_value() ); - - $this->assertEquals( 'EUR 100.00', (string) $amount ); } /** @@ -47,7 +45,7 @@ public function test_json() { $amount = Amount::from_json( $json_data ); - $json_string = wp_json_encode( $amount->get_json(), JSON_PRETTY_PRINT ); + $json_string = wp_json_encode( $amount->jsonSerialize(), JSON_PRETTY_PRINT ); $this->assertEquals( wp_json_encode( $json_data, JSON_PRETTY_PRINT ), $json_string ); @@ -60,7 +58,7 @@ public function test_json() { public function test_invalid_object_missing_currency() { $object = (object) [ 'value' => '100.00' ]; - $this->expectException( InvalidArgumentException::class ); + $this->expectException( Exception::class ); Amount::from_object( $object ); } @@ -71,7 +69,7 @@ public function test_invalid_object_missing_currency() { public function test_from_object_missing_value() { $object = (object) [ 'currency' => 'EUR' ]; - $this->expectException( InvalidArgumentException::class ); + $this->expectException( Exception::class ); Amount::from_object( $object ); } diff --git a/tests/src/AmountTransformerTest.php b/tests/src/AmountTransformerTest.php index 1583a3b..01c0bf2 100644 --- a/tests/src/AmountTransformerTest.php +++ b/tests/src/AmountTransformerTest.php @@ -26,15 +26,16 @@ class AmountTransformerTest extends \PHPUnit_Framework_TestCase { /** * Test transform. * - * @param Money $money Pronamic money. - * @param string $expected Expected value. - * + * @param Money $money Pronamic money. + * @param string $expected_currency Expected currency. + * @param string $expected_value Expected value. * @dataProvider amount_provider */ - public function test_transform( $money, $expected ) { + public function test_transform( $money, $expected_currency, $expected_value ) { $amount = AmountTransformer::transform( $money ); - $this->assertEquals( $expected, strval( $amount ) ); + $this->assertEquals( $expected_currency, $amount->get_currency() ); + $this->assertEquals( $expected_value, $amount->get_value() ); } /** @@ -44,8 +45,8 @@ public function test_transform( $money, $expected ) { */ public function amount_provider() { return [ - [ new Money( 100, 'EUR' ), 'EUR 100.00' ], - [ new Money( 5, 'BHD' ), 'BHD 5.000' ], + [ new Money( 100, 'EUR' ), 'EUR', '100.00' ], + [ new Money( 5, 'BHD' ), 'BHD', '5.000' ], ]; } } diff --git a/tests/src/GatewayTest.php b/tests/src/GatewayTest.php index 7b5da17..e3b9924 100644 --- a/tests/src/GatewayTest.php +++ b/tests/src/GatewayTest.php @@ -49,27 +49,19 @@ class GatewayTest extends WP_UnitTestCase { /** * Setup gateway test. */ - public function setUp() { - parent::setUp(); + public function set_up() { + parent::set_up(); $this->factory = new Factory(); - $this->factory->fake( - 'https://api.mollie.com/v2/methods', - function( Request $request ) { - $file = __DIR__ . '/../http/api-mollie-com-v2-methods.http'; + $this->factory->fake( 'https://api.mollie.com/v2/methods/ideal?include=issuers', __DIR__ . '/../http/api-mollie-com-v2-methods-ideal.http' ); - $body = $request->body(); - - if ( \is_array( $body ) && \array_key_exists( 'sequenceType', $body ) ) { - $sequence_type = $body['sequenceType']; - - $file = __DIR__ . \sprintf( '/../http/api-mollie-com-v2-methods-%s.http', $sequence_type ); - } - - return Response::array_from_file( $file ); - } - ); + $this->factory->fake( 'https://api.mollie.com/v2/methods?includeWallets=applepay&resource=payments&sequenceType=oneoff', __DIR__ . '/../http/api-mollie-com-v2-methods-oneoff.http' ); + $this->factory->fake( 'https://api.mollie.com/v2/methods?includeWallets=applepay&resource=payments&sequenceType=recurring', __DIR__ . '/../http/api-mollie-com-v2-methods-recurring.http' ); + $this->factory->fake( 'https://api.mollie.com/v2/methods?includeWallets=applepay&resource=payments&sequenceType=first', __DIR__ . '/../http/api-mollie-com-v2-methods-recurring.http' ); + $this->factory->fake( 'https://api.mollie.com/v2/methods?includeWallets=applepay&resource=orders&sequenceType=oneoff', __DIR__ . '/../http/api-mollie-com-v2-methods-oneoff.http' ); + $this->factory->fake( 'https://api.mollie.com/v2/methods?includeWallets=applepay&resource=orders&sequenceType=recurring', __DIR__ . '/../http/api-mollie-com-v2-methods-recurring.http' ); + $this->factory->fake( 'https://api.mollie.com/v2/methods?includeWallets=applepay&resource=orders&sequenceType=first', __DIR__ . '/../http/api-mollie-com-v2-methods-recurring.http' ); $this->set_gateway( [ @@ -104,7 +96,7 @@ private function set_gateway( $args = [] ) { public function test_get_issuers_type() { $issuers = $this->gateway->get_issuers(); - $this->assertInternalType( 'array', $issuers ); + $this->assertIsArray( $issuers ); } /** @@ -115,11 +107,11 @@ public function test_get_issuers_type() { public function test_get_issuers_structure() { $issuers = $this->gateway->get_issuers(); - $this->assertInternalType( 'array', $issuers ); + $this->assertIsArray( $issuers ); // Check issuers array structure. if ( ! empty( $issuers ) ) { - $this->assertInternalType( 'array', $issuers[0] ); + $this->assertIsArray( $issuers[0] ); $this->assertArrayHasKey( 'options', $issuers[0] ); } } @@ -131,7 +123,7 @@ public function test_get_supported_payment_methods_type() { $supported = $this->gateway->get_supported_payment_methods(); // Assert payment methods array type. - $this->assertInternalType( 'array', $supported ); + $this->assertIsArray( $supported ); } /** @@ -159,7 +151,7 @@ public function test_get_supported_payment_methods_valid() { public function test_get_available_payment_methods_type() { $available = $this->gateway->get_available_payment_methods(); - $this->assertInternalType( 'array', $available ); + $this->assertIsArray( $available ); } /** @@ -184,18 +176,19 @@ public function test_get_available_payment_methods_valid() { * Test webhook url. * * @param string $home_url Home URL. + * @param Payment $payment Payment. * @param string|null $expected Expected value. * * @dataProvider webhook_url_provider */ - public function test_webhook_url( $home_url, $expected ) { + public function test_webhook_url( $home_url, Payment $payment, $expected ) { $filter_home_url = function( $url ) use ( $home_url ) { return $home_url; }; add_filter( 'home_url', $filter_home_url ); - $this->assertEquals( $expected, $this->gateway->get_webhook_url() ); + $this->assertEquals( $expected, $this->gateway->get_webhook_url( $payment ) ); remove_filter( 'home_url', $filter_home_url ); } @@ -215,15 +208,30 @@ public function webhook_url_provider() { add_filter( 'home_url', $filter_home_url ); - $webhook_url = \rest_url( Integration::REST_ROUTE_NAMESPACE . '/webhook' ); + // Payments resource. + $payment = new Payment(); + + $payment->set_id( 1 ); + + $payments_webhook_url = \rest_url( Integration::REST_ROUTE_NAMESPACE . '/payments/webhook/1' ); + + // Orders resource. + $order_payment = new Payment(); + + $order_payment->set_id( 1 ); + $order_payment->set_payment_method( PaymentMethods::KLARNA_PAY_LATER ); + $order_payment->set_source( 'memberpress_transaction' ); + + $order_payment_webhook_url = \rest_url( Integration::REST_ROUTE_NAMESPACE . '/orders/webhook/1' ); remove_filter( 'home_url', $filter_home_url ); return [ - [ $home_url, $webhook_url ], - [ 'https://localhost/', null ], - [ 'https://example.dev/', null ], - [ 'https://example.local/', null ], + [ $home_url, $order_payment, $order_payment_webhook_url ], + [ $home_url, $payment, $payments_webhook_url ], + [ 'https://localhost/', $payment, null ], + [ 'https://example.dev/', $payment, null ], + [ 'https://example.local/', $payment, null ], ]; } @@ -300,7 +308,6 @@ public function get_customer_id_for_payment_provider() { [ 0, null, null, false ], [ 10, null, null, false ], [ 1, null, null, false ], - [ 1, null, $cst_first, $cst_first ], [ 1, $cst_subscription, null, $cst_subscription ], [ 1, $cst_subscription, $cst_first, $cst_subscription ], [ '1', $cst_subscription, $cst_first, $cst_subscription ], @@ -335,7 +342,6 @@ public function test_copy_customer_id_to_wp_user( $config_id, $user_id, $custome $customer->set_user_id( $user_id ); $subscription = new Subscription(); - $subscription->set_id( 1 ); $subscription->set_customer( $customer ); $subscriptions_data_store = new SubscriptionsDataStoreCPT(); @@ -350,7 +356,7 @@ public function test_copy_customer_id_to_wp_user( $config_id, $user_id, $custome // Get customer ID from user meta. $user_customer_ids = $this->gateway->get_customer_ids_for_user( $user_id ); - $this->assertInternalType( 'array', $user_customer_ids ); + $this->assertIsArray( $user_customer_ids ); if ( is_string( $expected ) ) { $this->assertContains( $expected, $user_customer_ids ); diff --git a/tests/src/IntegrationTest.php b/tests/src/IntegrationTest.php index e098813..26134a4 100644 --- a/tests/src/IntegrationTest.php +++ b/tests/src/IntegrationTest.php @@ -30,7 +30,9 @@ class IntegrationTest extends WP_UnitTestCase { /** * Setup. */ - public function setUp() { + public function set_up() { + parent::set_up(); + $this->integration = new Integration(); } @@ -58,7 +60,7 @@ public function test_get_name() { public function test_get_settings() { $settings = $this->integration->get_settings(); - $this->assertInternalType( 'array', $settings ); + $this->assertIsArray( $settings ); } /** @@ -111,6 +113,6 @@ public function test_gateway() { * Test settings. */ public function test_settings() { - $this->assertInternalType( 'array', $this->integration->get_settings_fields() ); + $this->assertIsArray( $this->integration->get_settings_fields() ); } } diff --git a/tests/src/OrderRequestTest.php b/tests/src/OrderRequestTest.php new file mode 100644 index 0000000..e8e0b81 --- /dev/null +++ b/tests/src/OrderRequestTest.php @@ -0,0 +1,135 @@ + + * @copyright 2005-2022 Pronamic + * @license GPL-3.0-or-later + * @package Pronamic\WordPress\Pay + */ + +namespace Pronamic\WordPress\Pay\Gateways\Mollie; + +use Pronamic\WordPress\Pay\Address as Core_Address; +use Pronamic\WordPress\Pay\ContactName; +use Pronamic\WordPress\Number\Number; +use Yoast\PHPUnitPolyfills\TestCases\TestCase; + +/** + * Order request test + * + * @author Reüel van der Steege + * @version 4.3.0 + * @since 4.3.0 + */ +class OrderRequestTest extends TestCase { + /** + * Order request. + * + * @var OrderRequest + */ + private $request; + + /** + * Setup. + */ + public function set_up() : void { + parent::set_up(); + + $lines = new Lines(); + + $lines->new_line( + 'Test product', + 1, + new Amount( 'EUR', '100.00' ), + new Amount( 'EUR', '121.00' ), + Number::from_mixed( '21.00' ), + new Amount( 'EUR', '21.00' ) + ); + + $request = new OrderRequest( + new Amount( 'EUR', '121.00' ), + '12345', + $lines, + 'nl_NL' + ); + + $billing_address = new Core_Address(); + + $name = new ContactName(); + + $name->set_first_name( 'Remco' ); + $name->set_last_name( 'Tolsma' ); + + $billing_address->set_name( $name ); + $billing_address->set_company_name( 'Pronamic' ); + $billing_address->set_email( 'info@pronamic.nl' ); + $billing_address->set_line_1( 'Burgemeester Wuiteweg 39b' ); + $billing_address->set_city( 'Drachten' ); + $billing_address->set_country_code( 'NL' ); + $billing_address->set_postal_code( '9203 KA' ); + $billing_address->set_region( 'Friesland' ); + $billing_address->set_phone( '085 40 11 580' ); + + $request->set_billing_address( Address::from_wp_address( $billing_address ) ); + + $request->redirect_url = 'https://example.com/mollie-redirect/'; + $request->webhook_url = 'https://example.com/mollie-webhook/'; + $request->method = Methods::KLARNA_PAY_LATER; + + $this->request = $request; + } + + /** + * Test order request. + * + * @return void + */ + public function test_order_request() { + $this->assertEquals( + (object) [ + 'amount' => (object) [ + 'currency' => 'EUR', + 'value' => '121.00', + ], + 'orderNumber' => '12345', + 'lines' => [ + (object) [ + 'name' => 'Test product', + 'quantity' => 1, + 'unitPrice' => (object) [ + 'currency' => 'EUR', + 'value' => '100.00', + ], + 'totalAmount' => (object) [ + 'currency' => 'EUR', + 'value' => '121.00', + ], + 'vatRate' => '21.00', + 'vatAmount' => (object) [ + 'currency' => 'EUR', + 'value' => '21.00', + ], + ], + ], + 'locale' => 'nl_NL', + 'billingAddress' => (object) [ + 'organizationName' => 'Pronamic', + 'givenName' => 'Remco', + 'familyName' => 'Tolsma', + 'email' => 'info@pronamic.nl', + 'phone' => '085 40 11 580', + 'streetAndNumber' => 'Burgemeester Wuiteweg 39b', + 'postalCode' => '9203 KA', + 'city' => 'Drachten', + 'region' => 'Friesland', + 'country' => 'NL', + ], + 'redirectUrl' => 'https://example.com/mollie-redirect/', + 'webhookUrl' => 'https://example.com/mollie-webhook/', + 'method' => Methods::KLARNA_PAY_LATER, + ], + $this->request->jsonSerialize() + ); + } +} diff --git a/tests/src/PaymentRequestTest.php b/tests/src/PaymentRequestTest.php index 379ef3f..7e64f85 100644 --- a/tests/src/PaymentRequestTest.php +++ b/tests/src/PaymentRequestTest.php @@ -10,6 +10,8 @@ namespace Pronamic\WordPress\Pay\Gateways\Mollie; +use WP_UnitTestCase; + /** * Payment request test * @@ -17,7 +19,7 @@ * @version 2.1.4 * @since 1.0.0 */ -class PaymentRequestTest extends \PHPUnit_Framework_TestCase { +class PaymentRequestTest extends WP_UnitTestCase { /** * Payment request. * @@ -28,7 +30,9 @@ class PaymentRequestTest extends \PHPUnit_Framework_TestCase { /** * Setup. */ - public function setUp() { + public function set_up() { + parent::set_up(); + $request = new PaymentRequest( new Amount( 'EUR', '100.00' ), 'Test' @@ -52,7 +56,7 @@ public function setUp() { public function test_payment_request() { $this->assertEquals( [ - 'amount' => $this->request->amount->get_json(), + 'amount' => $this->request->amount->jsonSerialize(), 'description' => 'Test', 'redirectUrl' => 'https://example.com/mollie-redirect/', 'webhookUrl' => 'https://example.com/mollie-webhook/', @@ -63,7 +67,7 @@ public function test_payment_request() { 'customerId' => 'cst_8wmqcHMN4U', 'sequenceType' => 'first', ], - $this->request->get_array() + (array) $this->request->jsonSerialize() ); } @@ -79,7 +83,7 @@ public function test_due_date() { $this->assertEquals( [ - 'amount' => $this->request->amount->get_json(), + 'amount' => $this->request->amount->jsonSerialize(), 'description' => 'Test', 'redirectUrl' => 'https://example.com/mollie-redirect/', 'webhookUrl' => 'https://example.com/mollie-webhook/', @@ -91,7 +95,7 @@ public function test_due_date() { 'customerId' => 'cst_8wmqcHMN4U', 'sequenceType' => 'first', ], - $this->request->get_array() + (array) $this->request->jsonSerialize() ); } @@ -113,11 +117,11 @@ public function test_billing_email() { $this->assertEquals( [ - 'amount' => $request->amount->get_json(), + 'amount' => $request->amount->jsonSerialize(), 'description' => 'Test', 'billingEmail' => 'john@example.com', ], - $request->get_array() + (array) $request->jsonSerialize() ); } @@ -144,11 +148,11 @@ public function test_metadata() { $this->assertEquals( [ - 'amount' => $request->amount->get_json(), + 'amount' => $request->amount->jsonSerialize(), 'description' => 'Test', 'metadata' => $metadata, ], - $request->get_array() + (array) $request->jsonSerialize() ); } diff --git a/tests/src/StatusesTest.php b/tests/src/StatusesTest.php index defc477..6d0671e 100644 --- a/tests/src/StatusesTest.php +++ b/tests/src/StatusesTest.php @@ -45,7 +45,7 @@ public function test_transform( $mollie_status, $expected ) { */ public function status_matrix_provider() { return [ - [ Statuses::AUTHORIZED, null ], + [ Statuses::AUTHORIZED, PaymentStatus::AUTHORIZED ], [ Statuses::OPEN, PaymentStatus::OPEN ], [ Statuses::CANCELED, PaymentStatus::CANCELLED ], [ Statuses::PAID, PaymentStatus::SUCCESS ], diff --git a/vendor-bin/phpstan/composer.json b/vendor-bin/phpstan/composer.json index ec6f3e5..fb25c16 100644 --- a/vendor-bin/phpstan/composer.json +++ b/vendor-bin/phpstan/composer.json @@ -1,6 +1,12 @@ { "require-dev": { - "phpstan/phpstan": "^0.12", - "szepeviktor/phpstan-wordpress": "^0.7" + "phpstan/phpstan": "^1.7", + "szepeviktor/phpstan-wordpress": "^1.1", + "phpstan/extension-installer": "^1.1" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } } } diff --git a/vendor-bin/phpstan/composer.lock b/vendor-bin/phpstan/composer.lock index c151b94..926b843 100644 --- a/vendor-bin/phpstan/composer.lock +++ b/vendor-bin/phpstan/composer.lock @@ -4,29 +4,32 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f2975d5a40e0b75bbe13237236457f2a", + "content-hash": "39dca6e8750202c05e07bacbb6d9e0ae", "packages": [], "packages-dev": [ { "name": "php-stubs/wordpress-stubs", - "version": "v5.8.2", + "version": "v5.9.3", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "67fd773742b7be5b4463f40318b0b4890a07033b" + "reference": "18d56875e5078a50b8ea4bc4b20b735ca61edeee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/67fd773742b7be5b4463f40318b0b4890a07033b", - "reference": "67fd773742b7be5b4463f40318b0b4890a07033b", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/18d56875e5078a50b8ea4bc4b20b735ca61edeee", + "reference": "18d56875e5078a50b8ea4bc4b20b735ca61edeee", "shasum": "" }, "replace": { "giacocorsiglia/wordpress-stubs": "*" }, "require-dev": { - "giacocorsiglia/stubs-generator": "^0.5.0", - "php": "~7.1" + "nikic/php-parser": "< 4.12.0", + "php": "~7.3 || ~8.0", + "php-stubs/generator": "^0.8.1", + "phpdocumentor/reflection-docblock": "^5.3", + "phpstan/phpstan": "^1.2" }, "suggest": { "paragonie/sodium_compat": "Pure PHP implementation of libsodium", @@ -47,26 +50,71 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v5.8.2" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v5.9.3" }, - "time": "2021-11-11T13:57:00+00:00" + "time": "2022-04-06T15:33:59+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "66c7adc9dfa38b6b5838a9fb728b68a7d8348051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/66c7adc9dfa38b6b5838a9fb728b68a7d8348051", + "reference": "66c7adc9dfa38b6b5838a9fb728b68a7d8348051", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0", + "php": "^7.1 || ^8.0", + "phpstan/phpstan": ">=0.11.6" + }, + "require-dev": { + "composer/composer": "^1.8", + "phing/phing": "^2.16.3", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.1.0" + }, + "time": "2020-12-13T13:06:13+00:00" }, { "name": "phpstan/phpstan", - "version": "0.12.99", + "version": "1.7.14", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "b4d40f1d759942f523be267a1bab6884f46ca3f7" + "reference": "e6f145f196a59c7ca91ea926c87ef3d936c4305f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b4d40f1d759942f523be267a1bab6884f46ca3f7", - "reference": "b4d40f1d759942f523be267a1bab6884f46ca3f7", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e6f145f196a59c7ca91ea926c87ef3d936c4305f", + "reference": "e6f145f196a59c7ca91ea926c87ef3d936c4305f", "shasum": "" }, "require": { - "php": "^7.1|^8.0" + "php": "^7.2|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -76,11 +124,6 @@ "phpstan.phar" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.12-dev" - } - }, "autoload": { "files": [ "bootstrap.php" @@ -93,7 +136,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.99" + "source": "https://github.com/phpstan/phpstan/tree/1.7.14" }, "funding": [ { @@ -113,20 +156,20 @@ "type": "tidelift" } ], - "time": "2021-09-12T20:09:55+00:00" + "time": "2022-06-14T13:09:35+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", - "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85", + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85", "shasum": "" }, "require": { @@ -135,7 +178,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -143,12 +186,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -176,7 +219,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0" }, "funding": [ { @@ -192,33 +235,34 @@ "type": "tidelift" } ], - "time": "2021-06-05T21:20:04+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v0.7.7", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "bdbea69b2ba4a69998c3b6fe2b7106d78a23bd72" + "reference": "99cfea6b4c15af12dec09110b25dbc7521363f78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/bdbea69b2ba4a69998c3b6fe2b7106d78a23bd72", - "reference": "bdbea69b2ba4a69998c3b6fe2b7106d78a23bd72", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/99cfea6b4c15af12dec09110b25dbc7521363f78", + "reference": "99cfea6b4c15af12dec09110b25dbc7521363f78", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", + "php": "^7.2 || ^8.0", "php-stubs/wordpress-stubs": "^4.7 || ^5.0", - "phpstan/phpstan": "^0.12.26", + "phpstan/phpstan": "^1.6", "symfony/polyfill-php73": "^1.12.0" }, "require-dev": { - "composer/composer": "^1.10.22", + "composer/composer": "^2.1.14", "dealerdirect/phpcodesniffer-composer-installer": "^0.7", "php-parallel-lint/php-parallel-lint": "^1.1", - "phpstan/phpstan-strict-rules": "^0.12", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^8 || ^9", "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.6" }, "type": "phpstan-extension", @@ -248,7 +292,7 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v0.7.7" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.1.1" }, "funding": [ { @@ -256,7 +300,7 @@ "type": "custom" } ], - "time": "2021-07-14T09:19:15+00:00" + "time": "2022-05-11T18:41:40+00:00" } ], "aliases": [], @@ -266,5 +310,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } diff --git a/views/meta-box-payment.php b/views/meta-box-payment.php index 9c1c15c..a0d2e7d 100644 --- a/views/meta-box-payment.php +++ b/views/meta-box-payment.php @@ -21,6 +21,7 @@ $mollie_payment_id = $payment->get_transaction_id(); $mollie_customer_id = $payment->get_meta( 'mollie_customer_id' ); $mollie_mandate_id = $payment->get_meta( 'mollie_mandate_id' ); +$mollie_order_id = $payment->get_meta( 'mollie_order_id' ); ?>

@@ -106,3 +107,21 @@ + + + +

+ +
+ + diff --git a/views/page-payment.php b/views/page-payment.php index 406c8e2..0744a75 100644 --- a/views/page-payment.php +++ b/views/page-payment.php @@ -14,6 +14,8 @@ $mollie_payment_id = \filter_input( INPUT_GET, 'id', FILTER_SANITIZE_STRING ); +$mollie_payment = null; + $payment = \get_pronamic_payment_by_transaction_id( $mollie_payment_id ); $command_curl = null; @@ -149,6 +151,23 @@ + + + + + + get_mode() ); ?> + + + + + +
get_metadata(), \JSON_PRETTY_PRINT ) ); ?>
+ + + + + is_debug_mode() ) : ?>