Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new PayPal API #135

Merged
merged 61 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
d4c736c
Begin implementing new Paypal API
gbirke May 3, 2023
30fa5bb
Add more tests for GuzzlePaypalAPI
gbirke May 4, 2023
9bf5af6
Refactor GuzzlePaypalAPITest
gbirke May 4, 2023
63d6deb
Refactor GuzzlePaypalAPI
gbirke May 4, 2023
6e49771
Remove OAuth2 authentication
gbirke May 4, 2023
68c6618
Implement Product List functionality
gbirke May 5, 2023
6ef4e88
Add createProduct to API
gbirke May 5, 2023
8900dd6
Add SubscriptionPlan Model
gbirke May 26, 2023
3b9a8a8
Unserialize SubscriptionPlan from PayPal JSON
gbirke May 26, 2023
5122769
Add listSubscriptionPlansForProduct method
gbirke May 26, 2023
a49d587
Add more unit tests for the PaypalAPI implementation
moiikana May 26, 2023
62a6e5f
Adapt PayPal code to new PHPStan rules
gbirke Jun 12, 2023
e8460c3
Implement CreateSubscriptionPlansForProduct use case
gbirke Jun 12, 2023
b312e8b
Implement CreateSubscriptionPlanForProduct in GuzzlePayPalAPI
moiikana Jun 13, 2023
822d4f5
Implement creation of plans for product
moiikana Jun 13, 2023
e93efa1
Change tests for CreateSubscriptionPlansForProductUseCase
moiikana Jun 14, 2023
72c5a72
Fix some inconsistencies
gbirke Jun 14, 2023
5bd9309
Improve types
gbirke Jun 15, 2023
b6c02f3
Add command skeleton
gbirke Jun 15, 2023
2139ee8
Experiemnt with symfony dotenv
gbirke Jun 15, 2023
dfc49de
Expland ListSubscriptionPlansCommand code
gbirke Jun 15, 2023
0d61603
Fix GuzzlePaypalAPI
gbirke Jun 16, 2023
9c736ac
Add CreateSubscriptionPlansCommand
gbirke Jun 16, 2023
156f5c7
Improve output of CLI
gbirke Jun 19, 2023
47d5667
Start outlining possible subscription API
gbirke Jun 19, 2023
75128c7
Implement createSubscription in GuzzlePaypalAPI
gbirke Jun 20, 2023
ca04176
Implement createOrder method for PaypalAPI
gbirke Jun 21, 2023
0f536c6
Adapt PayPal API requests after testing
gbirke Jun 21, 2023
b3ba337
Start implementing PayPalAPIUrlGenerator
gbirke Jun 21, 2023
dbd8923
Continue with URL Generator
gbirke Jun 22, 2023
fcdddd5
Prepare configuration for PaypalAPIURLGenerator
moiikana Jun 22, 2023
aec1e0e
Refactor PayPalAPIConfigFactory
gbirke Jun 23, 2023
c099940
Validate configuration file
gbirke Jun 23, 2023
35b742c
Improve CreateSubscriptionPlansCommand
gbirke Jul 7, 2023
469b5c1
Update documentation for PayPal API
gbirke Jul 10, 2023
f3b93cc
Fix PayPalAPIURLGeneratorConfigFactory
gbirke Jul 19, 2023
206a76b
Improve example config file
gbirke Jul 19, 2023
b224d1d
Improve validation of PPL API config
gbirke Aug 1, 2023
6eb7654
Improve PPL Order request
gbirke Aug 9, 2023
a0c4d56
Fix architecture after rebase
gbirke Aug 14, 2023
f7e40db
Introduce PayPal Payment identifiers
gbirke Aug 14, 2023
8c22198
Add repository for PayPalPaymentIdentifier
gbirke Aug 14, 2023
a3ca730
Create PaymentProviderAdapter
gbirke Aug 14, 2023
74073e4
Add doctrine cli
gbirke Aug 15, 2023
87c58a8
Create PaymentProviderAdapterFactory
gbirke Aug 15, 2023
cf5b78d
Use PaymentProviderAdapterFactory in use case
gbirke Aug 15, 2023
121a7f6
Change PaymentURLFactory
gbirke Aug 15, 2023
8cc1389
Introduce DomainSpecificContext in payment request
gbirke Aug 16, 2023
492e699
Use DomainSpecificContext for PayPal
gbirke Aug 16, 2023
f03807a
Use DomainSpecificContext for UrlGenerator
gbirke Aug 16, 2023
744a698
Allow authentication parameter passing
gbirke Aug 18, 2023
720ae5b
Replace RequestContext
gbirke Aug 18, 2023
c38ad2b
Split PaymentCreationRequest
gbirke Aug 21, 2023
e714f95
Improve UrlGeneratorFactory
gbirke Aug 22, 2023
9ff1824
Rename PayPalURLGeneratorConfigReader
gbirke Aug 22, 2023
bf5d1ef
Fix PayPalPaymentProviderAdapter
gbirke Aug 22, 2023
f4b806f
Rename request classes
gbirke Oct 9, 2023
946c6bf
Fix Coding Style after rebase
gbirke Oct 12, 2023
11fc15e
Add Order ID to PayPalOrder
gbirke Oct 12, 2023
fa8fc57
Add migration for payment_paypal_identifier
gbirke Oct 12, 2023
b68c008
Always return payment completion URL
gbirke Oct 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# URL of the paypal API v1 endpoint
PAYPAL_API_URL="https://example.com/"

# See https://developer.paypal.com/api/rest/authentication/
PAYPAL_API_CLIENT_ID="something"
PAYPAL_API_CLIENT_SECRET="n0ts0s3cr1t"

# Database connection string for connecting to a local database wit the the doctrine CLI
# Only needed if you're using the bin/doctrine script in the bounded context
DB_DSN="mysqli:/user:password@dtabase-server/database-name"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ coverage/

.idea/

.env

15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Code Coverage](https://scrutinizer-ci.com/g/wmde/fundraising-payments/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/wmde/fundraising-payments/?branch=master)

Bounded Context for the Wikimedia Deutschland fundraising payment (sub-)domain. Used by the
[user facing donation application](https://github.com/wmde/FundraisingFrontend) and the
[user facing donation application](https://github.com/wmde/fundraising-application) and the
"Fundraising Operations Center" (which is not public software).

## Installation
Expand All @@ -22,6 +22,19 @@ file that just defines a dependency on Fundraising Payments 1.x:
}
```

## Setting up the PayPal API
The payment context calls the PayPal REST API to create payments.
These API calls need credentials and a one-time setup of subscription plans
(i.e. definition of recurring payments) on the PayPal server.
There is a command line tool to do the subscription plan setup.
You can call this command (`create-subscription-plans`) from the console in [Fundraising Application](https://github.com/wmde/fundraising-application)
or from the `bin/console` file in this bounded context.

There is another command, `list-subscription-plans` that lists all the configured plans.

See [Configuring the PayPal API](docs/paypal_api.md) for more details on these commands and their configuration.


## Development

This project has a [Makefile](Makefile) that runs all tasks in Docker containers via
Expand Down
41 changes: 41 additions & 0 deletions bin/console
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env php
<?php

require __DIR__.'/../vendor/autoload.php';

use GuzzleHttp\Client;
use Psr\Log\NullLogger;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Dotenv\Dotenv;
use WMDE\Fundraising\PaymentContext\Services\PayPal\GuzzlePaypalAPI;

$dotenv = new Dotenv();
$dotenv->load( __DIR__ . '/../.env' );

function createPayPalAPI(): \WMDE\Fundraising\PaymentContext\Services\PayPal\PayPalAPI {
$clientId = $_ENV['PAYPAL_API_CLIENT_ID'] ?? '';
$secret = $_ENV['PAYPAL_API_CLIENT_SECRET'] ?? '';
$baseUri = $_ENV['PAYPAL_API_URL'] ?? '';
if ( !$clientId || !$secret || !$baseUri ) {
echo "You must put PAYPAL_API_URL, PAYPAL_API_CLIENT_ID and PAYPAL_API_CLIENT_SECRET\n";
exit( Command::FAILURE );
}

return new GuzzlePaypalAPI(
new Client( [ 'base_uri' => $baseUri ] ),
$clientId,
$secret,
new NullLogger()
);
}

$api = createPayPalAPI();

$application = new Application();

$application->add( new \WMDE\Fundraising\PaymentContext\Commands\ListSubscriptionPlansCommand( $api ) );
$application->add( new \WMDE\Fundraising\PaymentContext\Commands\CreateSubscriptionPlansCommand( $api ) );


$application->run();
42 changes: 42 additions & 0 deletions bin/doctrine
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env php
<?php

use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Tools\DsnParser;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
use Symfony\Component\Dotenv\Dotenv;
use WMDE\Fundraising\PaymentContext\PaymentContextFactory;

require __DIR__.'/../vendor/autoload.php';

$dotenv = new Dotenv();
$dotenv->load( __DIR__ . '/../.env' );

function createEntityManager(): EntityManager {
if (empty( $_ENV['DB_DSN'] ) ) {
echo "You must set the database connection string in 'DB_DSN'\n";
exit(1);
}
$dsnParser = new DsnParser(['mysql' => 'pdo_mysql']);
$connectionParams = $dsnParser
->parse( $_ENV['DB_DSN'] );
$connection = DriverManager::getConnection( $connectionParams );

$contextFactory = new PaymentContextFactory();
$contextFactory->registerCustomTypes( $connection );
$doctrineConfig = ORMSetup::createXMLMetadataConfiguration(
$contextFactory->getDoctrineMappingPaths(),
true
);

return new EntityManager( $connection, $doctrineConfig );
}


ConsoleRunner::run(
new SingleManagerProvider(createEntityManager()),
[]
);
11 changes: 8 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,22 @@
"doctrine/orm": "^2.16.1",
"doctrine/dbal": "^3.3",
"doctrine/migrations": "^3.5",
"guzzlehttp/guzzle": "^7.4",
"sofort/sofortlib-php": "^3.2",
"symfony/cache": "^6.0",
"guzzlehttp/guzzle": "^7.4"
"symfony/cache": "^6.3",
"symfony/console": "^6.3",
"symfony/dotenv": "^6.3",
"symfony/yaml": "^6.3",
"symfony/config": "^6.3"
},
"require-dev": {
"phpunit/phpunit": "~10.1",
"wmde/fundraising-phpcs": "~9.0",
"wmde/inspector-generator": "dev-main",
"phpstan/phpstan": "~1.3",
"phpstan/phpstan-phpunit": "^1.0",
"qossmic/deptrac-shim": "^1.0"
"qossmic/deptrac-shim": "^1.0",
"wmde/psr-log-test-doubles": "~v3.2.0"
},
"repositories": [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="WMDE\Fundraising\PaymentContext\Domain\Model\PayPalOrder">
<field name="transactionId" type="string" column="transaction_id" nullable="true" />
<field name="orderId" type="string" column="order_id" nullable="false" />

<indexes>
<index name="transaction_id_idx" columns="transaction_id" />
<index name="order_id_idx" columns="order_id" />
</indexes>
</entity>
</doctrine-mapping>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="WMDE\Fundraising\PaymentContext\Domain\Model\PayPalPaymentIdentifier" table="payment_paypal_identifier" inheritance-type="SINGLE_TABLE">
<discriminator-column name="identifier_type" type="string" length="1"/>
<discriminator-map>
<discriminator-mapping value="S" class="WMDE\Fundraising\PaymentContext\Domain\Model\PayPalSubscription"/>
<discriminator-mapping value="O" class="WMDE\Fundraising\PaymentContext\Domain\Model\PayPalOrder"/>
</discriminator-map>
<id name="payment" association-key="true" />
<one-to-one field="payment" target-entity="WMDE\Fundraising\PaymentContext\Domain\Model\Payment" >
<cascade>
<cascade-persist />
</cascade>
</one-to-one>
</entity>
</doctrine-mapping>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="WMDE\Fundraising\PaymentContext\Domain\Model\PayPalSubscription">
<field name="subscriptionId" type="string" column="subscription_id" nullable="false" />

<indexes>
<index name="subscription_id_idx" columns="subscription_id" />
</indexes>
</entity>
</doctrine-mapping>
102 changes: 102 additions & 0 deletions config/paypal_api.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Configuration file for setting up subscription plans for different products and locales
#
# The first level of the configuration is the "product" level.
# In our application, we currently have two products "donation" and "membership".
# The distinction lies in the _names_ of the products and subscription plans,
# which will be shown on the PayPal page.
#
# The second level of the configuration is the locale level.
# Each product has to have definitions for each locale, e.g. de_DE or en_GB.

donation:
de_DE:
# `product_id` is an ID generated by us. It follows the pattern of <productName>_<languageName>_<number>
# <languageName> is the first part of the locale, e.g. 'de' in de_DE
# The CLI for creating subscription plans uses this product ID to
# reference the product for each subscription plan
product_id: 'donation_de_1'
# The localized product name, used both for subscription plans and one-time payments
# Currently not shown in the PayPal UI for subscription plans, only on the
# PayPal checkout page for one-time payments
product_name: 'Spende an Wikimedia'
subscription_plans:
# ID is generated automatically by PayPal when we create subscription plans with the CLI
# Before the creation, ID can be a stub, after the creation please insert the generated IDs
- id: '123'
# the (public) string that the user sees on the checkout page at PayPal
name: 'Monatliche Spende an Wikimedia'
# internal value, should be a valid string for type PaymentInterval
interval: 'Monthly'
- id: '496'
name: 'Vierteljährliche Spende an Wikimedia'
interval: 'Quarterly'
- id: '456'
name: 'Halbjährliche Spende an Wikimedia'
interval: 'HalfYearly'
- id: 'ABC'
name: 'Jährliche Spende an Wikimedia'
interval: 'Yearly'
# the page the user gets redirected to when the PayPal payment was successfully created
# Where do they get redirected from? From PayPal page
# The UrlAuthenticator will append access tokens to the URL
return_url: 'https://test-spenden.wikimedia.de/show-donation-confirmation?'
# the page the user gets redirected to when they hit 'cancel'
# Where do they get redirected from? From PayPal page
# The UrlAuthenticator will append access tokens to the URL
cancel_url: 'https://test-spenden.wikimedia.de/new?id='

en_GB:
product_id: 'donation_en_1'
product_name: 'Donation to Wikimedia'
subscription_plans:
- id: '786'
name: 'Monthly donation to Wikimedia'
interval: 'Monthly'
- id: 'GHI'
name: 'Quarterly donation to Wikimedia'
interval: 'Quarterly'
- id: 'JKL'
name: 'Half yearly donation to Wikimedia'
interval: 'HalfYearly'
- id: 'NMP'
name: 'Yearly donation to Wikimedia'
interval: 'Yearly'
return_url: 'https://test-spenden.wikimedia.de/show-donation-confirmation?&lang=en'
cancel_url: 'https://test-spenden.wikimedia.de/new?&lang=en'
membership:
de_DE:
product_id: 'membership_de_1'
product_name: 'Mitgliedschaftsbeitrag an Wikimedia'
subscription_plans:
- id: '333'
name: 'Monatlicher Mitgliedschaftsbeitrag an Wikimedia'
interval: 'Monthly'
- id: 'MNT'
name: 'Vierteljährlicher Mitgliedschaftsbeitrag an Wikimedia'
interval: 'Quarterly'
- id: 'DEN'
name: 'Halbjährlicher Mitgliedschaftsbeitrag an Wikimedia'
interval: 'HalfYearly'
- id: 'MBB'
name: 'Jährlicher Mitgliedschaftsbeitrag an Wikimedia'
interval: 'Yearly'
return_url: 'https://test-spenden.wikimedia.de/show-membership-confirmation?'
cancel_url: 'https://test-spenden.wikimedia.de/apply-for-membership?'
en_GB:
product_id: 'membership_en_1'
product_name: 'Wikimedia Membership'
subscription_plans:
- id: '716'
name: 'Monthly membership fee for Wikimedia'
interval: 'Monthly'
- id: 'XZA'
name: 'Quarterly membership fee for Wikimedia'
interval: 'Quarterly'
- id: 'VCX'
name: 'Half yearly membership fee for Wikimedia'
interval: 'HalfYearly'
- id: 'MNB'
name: 'Yearly membership fee for Wikimedia'
interval: 'Yearly'
return_url: 'https://test-spenden.wikimedia.de/show-membership-confirmation?lang=en'
cancel_url: 'https://test-spenden.wikimedia.de/apply-for-membership?lang=en'
40 changes: 34 additions & 6 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,28 @@ deptrac:
collectors:
- type: classLike
value: WMDE\\Fundraising\\PaymentContext\\DataAccess.*
- name: ServiceInterface
- type: class
value: WMDE\\Fundraising\\PaymentContext\\ScalarTypeConverter
# Interfaces and value objects for services
- name: ServiceModel
collectors:
- type: interface
value: WMDE\\Fundraising\\PaymentContext\\Services.*
- type: class
value: WMDE\\Fundraising\\PaymentContext\\Services\\Paypal\\Model.*
- type: classLike
value: WMDE\\Fundraising\\PaymentContext\\Services\\PaymentUrlGenerator\\Sofort.*
- name: Service
collectors:
- type: class
value: WMDE\\Fundraising\\PaymentContext\\Services.*
- type: bool
must:
- type: class
value: WMDE\\Fundraising\\PaymentContext\\Services.*
must_not:
- type: class
value: WMDE\\Fundraising\\PaymentContext\\Services\\Paypal\\Model.*
- type: class
value: WMDE\\Fundraising\\PaymentContext\\Services\\PaymentUrlGenerator\\Sofort.*
# Domain libraries from WMDE
- name: DomainLibrary
collectors:
Expand All @@ -44,6 +58,14 @@ deptrac:
collectors:
- type: classNameRegex
value: /^Sofort\\.*/
- name: Psr
collectors:
- type: classNameRegex
value: /^Psr\\.*/
- name: Symfony Config
collectors:
- type: classNameRegex
value: /^Symfony\\Component\\(Config|Yaml)\\.*/
ruleset:
Domain:
- DomainLibrary
Expand All @@ -53,6 +75,7 @@ deptrac:
- ServiceInterface
- DomainLibrary
- DomainValidators
- ServiceModel
DataAccess:
- Domain
- DomainLibrary
Expand All @@ -67,24 +90,29 @@ deptrac:
# into the Domain layer.
- UseCase
- DataAccess
- ServiceInterface
- ServiceModel
# Maybe the Services should not directly depend on Doctrine but should go through the `DataAccess` layer
# TODO: Move DoctrineTransactionIdFinder.php and UniquePaymentReferenceCodeGenerator.php into DataAccess
- Doctrine
- Guzzle
- Sofort
ServiceInterface:
- Psr
- Symfony Config
ServiceModel:
- Domain
- DomainLibrary
- ServiceInterface
formatters:
graphviz:
groups:
Service:
- Service
- ServiceInterface
- ServiceModel
Vendor:
- Doctrine
- Guzzle
- Sofort
- Psr
- Symfony Config


Loading