Passed
Push — main ( 413103...4ffc18 )
by Iain
05:47
created

Billing::handleStripeConfig()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 36
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 20
c 0
b 0
f 0
nc 17
nop 2
dl 0
loc 36
rs 8.9777
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * Copyright Iain Cambridge 2020-2023.
7
 *
8
 * Use of this software is governed by the Business Source License included in the LICENSE file and at https://getparthenon.com/docs/next/license.
9
 *
10
 * Change Date: TBD ( 3 years after 2.2.0 release )
11
 *
12
 * On the date above, in accordance with the Business Source License, use of this software will be governed by the open source license specified in the LICENSE file.
13
 */
14
15
namespace Parthenon\DependencyInjection\Modules;
16
17
use Parthenon\Billing\Athena\CustomerTeamSection;
18
use Parthenon\Billing\Athena\CustomerUserSection;
19
use Parthenon\Billing\CustomerProviderInterface;
20
use Parthenon\Billing\Plan\CachedPlanManager;
21
use Parthenon\Billing\Plan\PlanManager;
22
use Parthenon\Billing\Plan\PlanManagerInterface;
23
use Parthenon\Billing\Repository\CustomerRepositoryInterface;
24
use Parthenon\Billing\TeamCustomerProvider;
25
use Parthenon\Billing\UserCustomerProvider;
26
use Parthenon\Common\Exception\ParameterNotSetException;
27
use Parthenon\User\Repository\TeamRepositoryInterface;
28
use Parthenon\User\Repository\UserRepositoryInterface;
29
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
30
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
31
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
32
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
33
use Symfony\Component\Config\FileLocator;
34
use Symfony\Component\DependencyInjection\ContainerBuilder;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Depend...ection\ContainerBuilder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
35
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Depend...on\Loader\XmlFileLoader was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
36
37
class Billing implements ModuleConfigurationInterface
38
{
39
    public function addConfig(NodeBuilder $nodeBuilder): void
40
    {
41
        $nodeBuilder->arrayNode('billing')
42
            ->children()
43
                ->booleanNode('enabled')->defaultFalse()->end()
44
                ?->scalarNode('customer_type')->defaultValue('team')->end()
0 ignored issues
show
Bug introduced by
The method scalarNode() does not exist on Symfony\Component\Config...der\NodeParentInterface. It seems like you code against a sub-type of Symfony\Component\Config...der\NodeParentInterface such as Symfony\Component\Config...ion\Builder\NodeBuilder. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

44
                ?->/** @scrutinizer ignore-call */ scalarNode('customer_type')->defaultValue('team')->end()
Loading history...
45
                ?->scalarNode('plan_management')->defaultValue('config')->end()
46
                ?->arrayNode('payments')
47
                    ->children()
48
                        ->scalarNode('provider')->end()
49
                        ?->booleanNode('pci_mode')->end()
50
                        ?->scalarNode('return_url')->end()
51
                        ?->scalarNode('cancel_url')->end()
52
                        ?->arrayNode('adyen')
53
                            ->children()
54
                                ->scalarNode('api_key')->end()
55
                                ?->scalarNode('merchant_account')->end()
56
                                ?->booleanNode('test_mode')->end()
57
                                ?->scalarNode('webhook_secret')->end()
58
                                ?->scalarNode('prefix')->end()
59
                                ?->scalarNode('cse_url')->end()
60
                            ?->end()
61
                        ->end()
62
                        ?->arrayNode('stripe')
63
                            ->children()
64
                                ->scalarNode('private_api_key')->end()
65
                                ->scalarNode('webhook_secret')->end()
66
                                ?->scalarNode('public_api_key')->end()
67
                                ?->scalarNode('product_id')->end()
68
                                ?->arrayNode('payment_methods')
69
                                    ->scalarPrototype()->end()
70
                                ?->end()
71
                            ->end()
72
                        ->end()
73
                    ->end()
74
                ->end()
75
                ->end()
76
                ->fixXmlConfig('plans')
77
                ->append($this->getPlansNode())
78
            ?->end();
79
    }
80
81
    public function handleDefaultParameters(ContainerBuilder $container): void
82
    {
83
        $container->setParameter('parthenon_billing_payments_obol_config', []);
84
        $container->setParameter('parthenon_billing_customer_type', 'team');
85
        $container->setParameter('parthenon_billing_config_frontend_info', '');
86
        $container->setParameter('parthenon_billing_config_webhook_secret', '');
87
        $container->setParameter('parthenon_billing_plan_plans', []);
88
        $container->setParameter('parthenon_billing_product_id', null);
89
    }
90
91
    public function handleConfiguration(array $config, ContainerBuilder $container): void
92
    {
93
        if (!isset($config['billing']) || !isset($config['billing']['enabled']) || false === $config['billing']['enabled']) {
94
            return;
95
        }
96
        $container->setParameter('parthenon_billing_enabled', true);
97
98
        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../../Resources/config'));
99
        $loader->load('services/billing.xml');
100
        $loader->load('services/orm/billing.xml');
101
102
        $billingConfig = $config['billing'];
103
        $paymentsConfig = $billingConfig['payments'];
104
105
        if ('team' === strtolower($billingConfig['customer_type'])) {
106
            $this->handleTeamCustomer($config, $container);
107
        } elseif ('user' === strtolower($billingConfig['customer_type'])) {
108
            $this->handleUserCustomer($config, $container);
109
        }
110
111
        if ('athena' === strtolower($billingConfig['plan_management'])) {
112
            $loader->load('services/billing/athena_plans.xml');
113
            $container->setAlias(PlanManagerInterface::class, CachedPlanManager::class);
114
        } else {
115
            $container->setAlias(PlanManagerInterface::class, PlanManager::class);
116
        }
117
118
        $container->setParameter('parthenon_billing_plan_plans', $config['billing']['plan']);
119
120
        $obolConfig = match ($paymentsConfig['provider']) {
121
            'stripe' => $this->handleStripeConfig($paymentsConfig, $container),
122
            'adyen' => $this->handleAdyen($paymentsConfig, $container),
123
            'custom' => [],
124
            default => throw new ParameterNotSetException('billing.payments.provider must be valid'),
125
        };
126
127
        $container->setParameter('parthenon_billing_payments_obol_config', $obolConfig);
128
        $container->setParameter('parthenon_billing_plan_plans', $config['billing']['plan']);
129
    }
130
131
    public function buildPricesNode()
132
    {
133
        $treeBuilder = new TreeBuilder('prices');
134
        $node = $treeBuilder->getRootNode();
135
136
        $priceNode = $node->requiresAtLeastOneElement()
0 ignored issues
show
Bug introduced by
The method requiresAtLeastOneElement() does not exist on Symfony\Component\Config...\Builder\NodeDefinition. It seems like you code against a sub-type of Symfony\Component\Config...\Builder\NodeDefinition such as Symfony\Component\Config...der\ArrayNodeDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

136
        $priceNode = $node->/** @scrutinizer ignore-call */ requiresAtLeastOneElement()
Loading history...
137
            ->useAttributeAsKey('payment_schedule')
138
            ->prototype('array');
139
        assert($priceNode instanceof ArrayNodeDefinition);
140
141
        $priceNode
142
            ->arrayPrototype()
143
                ->children()
144
                    ->scalarNode('amount')->end()
145
                    ->scalarNode('price_id')->end()
146
                    ->booleanNode('public')->defaultTrue()->end()
147
                ->end()
148
                ->end()
149
            ->end();
150
151
        return $node;
152
    }
153
154
    protected function handleTeamCustomer(array $config, ContainerBuilder $containerBuilder): void
0 ignored issues
show
Unused Code introduced by
The parameter $config is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

154
    protected function handleTeamCustomer(/** @scrutinizer ignore-unused */ array $config, ContainerBuilder $containerBuilder): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
155
    {
156
        $containerBuilder->setAlias(CustomerProviderInterface::class, TeamCustomerProvider::class);
157
        $containerBuilder->setAlias(CustomerRepositoryInterface::class, TeamRepositoryInterface::class);
158
        $containerBuilder->removeDefinition(CustomerUserSection::class);
159
    }
160
161
    protected function handleUserCustomer(array $config, ContainerBuilder $containerBuilder): void
0 ignored issues
show
Unused Code introduced by
The parameter $config is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

161
    protected function handleUserCustomer(/** @scrutinizer ignore-unused */ array $config, ContainerBuilder $containerBuilder): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
162
    {
163
        $containerBuilder->setAlias(CustomerProviderInterface::class, UserCustomerProvider::class);
164
        $containerBuilder->setAlias(CustomerRepositoryInterface::class, UserRepositoryInterface::class);
165
        $containerBuilder->removeDefinition(CustomerTeamSection::class);
166
    }
167
168
    protected function handleStripeConfig(array $paymentsConfig, ContainerBuilder $containerBuilder): array
169
    {
170
        if (!isset($paymentsConfig['stripe']['private_api_key'])) {
171
            throw new ParameterNotSetException('billing.payments.stripe.private_api_key must be set.');
172
        }
173
174
        $pciMode = false;
175
176
        if (isset($paymentsConfig['pci_mode'])) {
177
            $pciMode = $paymentsConfig['pci_mode'];
178
        }
179
180
        $config = [
181
            'provider' => 'stripe',
182
            'api_key' => $paymentsConfig['stripe']['private_api_key'],
183
            'pci_mode' => $pciMode,
184
        ];
185
186
        $containerBuilder->setParameter('parthenon_billing_product_id', $paymentsConfig['stripe']['product_id'] ?? null);
187
        $containerBuilder->setParameter('parthenon_billing_config_frontend_info', $paymentsConfig['stripe']['public_api_key']);
188
        $containerBuilder->setParameter('parthenon_billing_config_webhook_secret', $paymentsConfig['stripe']['webhook_secret'] ?? '');
189
190
        if (isset($paymentsConfig['stripe']['payment_methods'])) {
191
            $config['payment_methods'] = $paymentsConfig['stripe']['payment_methods'];
192
        }
193
194
        if (isset($paymentsConfig['return_url'])) {
195
            $config['success_url'] = $paymentsConfig['return_url'];
196
            $config['cancel_url'] = $paymentsConfig['return_url'];
197
        }
198
199
        if (isset($paymentsConfig['cancel_url'])) {
200
            $config['cancel_url'] = $paymentsConfig['cancel_url'];
201
        }
202
203
        return $config;
204
    }
205
206
    protected function handleAdyen(array $paymentsConfig, ContainerBuilder $containerBuilder): array
207
    {
208
        if (!isset($paymentsConfig['adyen']['api_key'])) {
209
            throw new ParameterNotSetException('billing.payments.adyen.api_key must be set.');
210
        }
211
        if (!isset($paymentsConfig['adyen']['merchant_account'])) {
212
            throw new ParameterNotSetException('billing.payments.adyen.merchant_account must be set.');
213
        }
214
215
        $pciMode = false;
216
        if (isset($paymentsConfig['pci_mode'])) {
217
            $pciMode = $paymentsConfig['pci_mode'];
218
        }
219
220
        $testMode = true;
221
        if (isset($paymentsConfig['adyen']['test_mode'])) {
222
            $testMode = $paymentsConfig['adyen']['test_mode'];
223
        }
224
225
        $config = [
226
            'provider' => 'adyen',
227
            'api_key' => $paymentsConfig['adyen']['api_key'],
228
            'merchant_account' => $paymentsConfig['adyen']['merchant_account'],
229
            'pci_mode' => $pciMode,
230
            'test_mode' => $testMode,
231
        ];
232
233
        if ($paymentsConfig['adyen']['prefix']) {
234
            $config['prefix'] = $paymentsConfig['adyen']['prefix'];
235
        }
236
237
        if (isset($paymentsConfig['return_url'])) {
238
            $config['return_url'] = $paymentsConfig['return_url'];
239
        }
240
241
        $containerBuilder->setParameter('parthenon_billing_config_frontend_info', $paymentsConfig['adyen']['cse_url']);
242
        $containerBuilder->setParameter('parthenon_billing_config_webhook_secret', $paymentsConfig['adyen']['webhook_secret'] ?? '');
243
244
        return $config;
245
    }
246
247
    private function getPlansNode(): NodeDefinition
248
    {
249
        $treeBuilder = new TreeBuilder('plan');
250
        $node = $treeBuilder->getRootNode();
251
252
        /** @var ArrayNodeDefinition $planNode */
253
        $planNode = $node
254
            ->requiresAtLeastOneElement()
255
            ->useAttributeAsKey('name')
256
            ->prototype('array');
257
258
        $planNode
259
            ->fixXmlConfig('limits')
260
                ->children()
261
                    ->booleanNode('is_free')->defaultFalse()->end()
262
                    ?->booleanNode('is_per_seat')->defaultFalse()->end()
0 ignored issues
show
Bug introduced by
The method booleanNode() does not exist on Symfony\Component\Config...der\NodeParentInterface. It seems like you code against a sub-type of Symfony\Component\Config...der\NodeParentInterface such as Symfony\Component\Config...ion\Builder\NodeBuilder. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

262
                    ?->/** @scrutinizer ignore-call */ booleanNode('is_per_seat')->defaultFalse()->end()
Loading history...
263
                    ?->booleanNode('public')->defaultTrue()->end()
264
                    ?->booleanNode('has_trial')->defaultFalse()->end()
265
                    ?->scalarNode('trial_length_days')->defaultValue(0)->end()
266
                    ?->scalarNode('user_count')->end()
267
                    ->arrayNode('features')
268
                        ->scalarPrototype()->end()
269
                    ->end()
270
                    ->arrayNode('limit')
271
                        ->useAttributeAsKey('name')
272
                        ->prototype('array')
273
                        ->children()
274
                            ->integerNode('limit')->end()
275
                            ->scalarNode('description')->end()
276
                        ->end()
277
                    ->end()
278
                ->end()
279
            ->append($this->buildPricesNode())
280
            ->end();
281
282
        return $node;
283
    }
284
}
285