Billing::handleConfiguration()   B
last analyzed

Complexity

Conditions 9
Paths 10

Size

Total Lines 48
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 9
eloc 34
c 6
b 0
f 0
nc 10
nop 2
dl 0
loc 48
rs 8.0555
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * Copyright (C) 2020-2025 Iain Cambridge
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU LESSER GENERAL PUBLIC LICENSE as published by
10
 * the Free Software Foundation, either version 2.1 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU Lesser General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20
 */
21
22
namespace Parthenon\DependencyInjection\Modules;
23
24
use Parthenon\Billing\Athena\CustomerTeamSection;
25
use Parthenon\Billing\Athena\CustomerUserSection;
26
use Parthenon\Billing\BillaBear\Webhook\ProcessorInterface;
27
use Parthenon\Billing\CustomerProviderInterface;
28
use Parthenon\Billing\Plan\CachedPlanManager;
29
use Parthenon\Billing\Plan\CounterInterface;
30
use Parthenon\Billing\Plan\PlanManager;
31
use Parthenon\Billing\Plan\PlanManagerInterface;
32
use Parthenon\Billing\Repository\CustomerRepositoryInterface;
33
use Parthenon\Billing\TeamCustomerProvider;
34
use Parthenon\Billing\UserCustomerProvider;
35
use Parthenon\Billing\Webhook\HandlerInterface;
36
use Parthenon\Common\Exception\ParameterNotSetException;
37
use Parthenon\User\Repository\TeamRepositoryInterface;
38
use Parthenon\User\Repository\UserRepositoryInterface;
39
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
40
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
41
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
42
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
43
use Symfony\Component\Config\FileLocator;
44
use Symfony\Component\DependencyInjection\ContainerBuilder;
45
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
46
47
class Billing implements ModuleConfigurationInterface
48
{
49
    public function addConfig(NodeBuilder $nodeBuilder): void
50
    {
51
        $nodeBuilder->arrayNode('billing')
52
            ->children()
53
                ->booleanNode('enabled')->defaultFalse()->end()
54
                ?->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

54
                ?->/** @scrutinizer ignore-call */ scalarNode('customer_type')->defaultValue('team')->end()
Loading history...
55
                ?->scalarNode('plan_management')->defaultValue('config')->end()
56
                ?->arrayNode('billabear')
57
                    ->children()
58
                        ->booleanNode('enabled')->defaultFalse()->end()
59
                        ->scalarNode('api_url')->end()
60
                        ->scalarNode('api_key')->end()
61
                    ->end()
62
                ->end()
63
                ?->arrayNode('payments')
64
                    ->children()
65
                        ->scalarNode('provider')->end()
66
                        ?->booleanNode('pci_mode')->end()
67
                        ?->scalarNode('return_url')->end()
68
                        ?->scalarNode('cancel_url')->end()
69
                        ?->arrayNode('adyen')
70
                            ->children()
71
                                ->scalarNode('api_key')->end()
72
                                ?->scalarNode('merchant_account')->end()
73
                                ?->booleanNode('test_mode')->end()
74
                                ?->scalarNode('webhook_secret')->end()
75
                                ?->scalarNode('prefix')->end()
76
                                ?->scalarNode('cse_url')->end()
77
                            ?->end()
78
                        ->end()
79
                        ?->arrayNode('stripe')
80
                            ->children()
81
                                ->scalarNode('private_api_key')->end()
82
                                ->scalarNode('webhook_secret')->end()
83
                                ?->scalarNode('public_api_key')->end()
84
                                ?->scalarNode('product_id')->end()
85
                                ?->arrayNode('payment_methods')
86
                                    ->scalarPrototype()->end()
87
                                ?->end()
88
                            ->end()
89
                        ->end()
90
                    ->end()
91
                ->end()
92
                ->end()
93
                ->fixXmlConfig('plans')
94
                ->append($this->getPlansNode())
95
            ?->end();
96
    }
97
98
    public function handleDefaultParameters(ContainerBuilder $container): void
99
    {
100
        $container->setParameter('parthenon_billing_payments_obol_config', []);
101
        $container->setParameter('parthenon_billing_customer_type', 'team');
102
        $container->setParameter('parthenon_billing_config_frontend_info', '');
103
        $container->setParameter('parthenon_billing_config_webhook_secret', '');
104
        $container->setParameter('parthenon_billing_plan_plans', []);
105
        $container->setParameter('parthenon_billing_product_id', null);
106
        $container->setParameter('parthenon_billing_billabear_enabled', false);
107
        $container->setParameter('parthenon_billing_billabear_api_url', false);
108
        $container->setParameter('parthenon_billing_billabear_api_key', false);
109
    }
110
111
    public function handleConfiguration(array $config, ContainerBuilder $container): void
112
    {
113
        if (!isset($config['billing']) || !isset($config['billing']['enabled']) || false === $config['billing']['enabled']) {
114
            return;
115
        }
116
117
        $container->registerForAutoconfiguration(CounterInterface::class)->addTag('parthenon.billing.plan.counter');
118
        $container->registerForAutoconfiguration(HandlerInterface::class)->addTag('parthenon.billing.webhooks.handler');
119
        $container->registerForAutoconfiguration(ProcessorInterface::class)->addTag('parthenon.billing.billabear.webhooks.handler');
120
121
        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../../Resources/config'));
122
        $loader->load('services/billing.xml');
123
        $loader->load('services/orm/billing.xml');
124
125
        $billingConfig = $config['billing'];
126
        $paymentsConfig = $billingConfig['payments'];
127
128
        if ('team' === strtolower($billingConfig['customer_type'])) {
129
            $this->handleTeamCustomer($config, $container);
130
        } elseif ('user' === strtolower($billingConfig['customer_type'])) {
131
            $this->handleUserCustomer($config, $container);
132
        }
133
134
        if (isset($billingConfig['billabear']) && $billingConfig['billabear']['enabled']) {
135
            $loader->load('services/billing/billabear.xml');
136
            $container->setAlias(PlanManagerInterface::class, CachedPlanManager::class);
137
            $this->handleBillaBearConfig($billingConfig['billabear'], $container);
138
            $container->setParameter('parthenon_billing_enabled', false);
139
        } elseif ('athena' === strtolower($billingConfig['plan_management'])) {
140
            $container->setParameter('parthenon_billing_enabled', true);
141
            $loader->load('services/billing/athena_plans.xml');
142
            $container->setAlias(PlanManagerInterface::class, CachedPlanManager::class);
143
        } else {
144
            $container->setParameter('parthenon_billing_enabled', true);
145
            $container->setAlias(PlanManagerInterface::class, PlanManager::class);
146
        }
147
148
        $container->setParameter('parthenon_billing_plan_plans', $config['billing']['plan']);
149
150
        $obolConfig = match ($paymentsConfig['provider']) {
151
            'stripe' => $this->handleStripeConfig($paymentsConfig, $container),
152
            'adyen' => $this->handleAdyen($paymentsConfig, $container),
153
            'custom' => [],
154
            default => throw new ParameterNotSetException('billing.payments.provider must be valid'),
155
        };
156
157
        $container->setParameter('parthenon_billing_payments_obol_config', $obolConfig);
158
        $container->setParameter('parthenon_billing_plan_plans', $config['billing']['plan']);
159
    }
160
161
    public function buildPricesNode()
162
    {
163
        $treeBuilder = new TreeBuilder('prices');
164
        $node = $treeBuilder->getRootNode();
165
166
        $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

166
        $priceNode = $node->/** @scrutinizer ignore-call */ requiresAtLeastOneElement()
Loading history...
167
            ->useAttributeAsKey('payment_schedule')
168
            ->prototype('array');
169
        assert($priceNode instanceof ArrayNodeDefinition);
170
171
        $priceNode
172
            ->arrayPrototype()
173
                ->children()
174
                    ->scalarNode('amount')->end()
175
                    ->scalarNode('price_id')->end()
176
                    ->booleanNode('public')->defaultTrue()->end()
177
                ->end()
178
                ->end()
179
            ->end();
180
181
        return $node;
182
    }
183
184
    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

184
    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...
185
    {
186
        $containerBuilder->setAlias(CustomerProviderInterface::class, TeamCustomerProvider::class);
187
        $containerBuilder->setAlias(CustomerRepositoryInterface::class, TeamRepositoryInterface::class);
188
        $containerBuilder->removeDefinition(CustomerUserSection::class);
189
    }
190
191
    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

191
    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...
192
    {
193
        $containerBuilder->setAlias(CustomerProviderInterface::class, UserCustomerProvider::class);
194
        $containerBuilder->setAlias(CustomerRepositoryInterface::class, UserRepositoryInterface::class);
195
        $containerBuilder->removeDefinition(CustomerTeamSection::class);
196
    }
197
198
    protected function handleBillaBearConfig(array $billabearConfig, ContainerBuilder $containerBuilder): void
199
    {
200
        if (true !== $billabearConfig['enabled']) {
201
            return;
202
        }
203
204
        $containerBuilder->setParameter('parthenon_billing_billabear_enabled', true);
205
        $containerBuilder->setParameter('parthenon_billing_billabear_api_key', $billabearConfig['api_key']);
206
        $containerBuilder->setParameter('parthenon_billing_billabear_api_url', $billabearConfig['api_url']);
207
    }
208
209
    protected function handleStripeConfig(array $paymentsConfig, ContainerBuilder $containerBuilder): array
210
    {
211
        if (!isset($paymentsConfig['stripe']['private_api_key'])) {
212
            throw new ParameterNotSetException('billing.payments.stripe.private_api_key must be set.');
213
        }
214
215
        $pciMode = false;
216
217
        if (isset($paymentsConfig['pci_mode'])) {
218
            $pciMode = $paymentsConfig['pci_mode'];
219
        }
220
221
        $config = [
222
            'provider' => 'stripe',
223
            'api_key' => $paymentsConfig['stripe']['private_api_key'],
224
            'pci_mode' => $pciMode,
225
        ];
226
227
        $containerBuilder->setParameter('parthenon_billing_product_id', $paymentsConfig['stripe']['product_id'] ?? null);
228
        $containerBuilder->setParameter('parthenon_billing_config_frontend_info', $paymentsConfig['stripe']['public_api_key']);
229
        $containerBuilder->setParameter('parthenon_billing_config_webhook_secret', $paymentsConfig['stripe']['webhook_secret'] ?? '');
230
231
        if (isset($paymentsConfig['stripe']['payment_methods'])) {
232
            $config['payment_methods'] = $paymentsConfig['stripe']['payment_methods'];
233
        }
234
235
        if (isset($paymentsConfig['return_url'])) {
236
            $config['success_url'] = $paymentsConfig['return_url'];
237
            $config['cancel_url'] = $paymentsConfig['return_url'];
238
        }
239
240
        if (isset($paymentsConfig['cancel_url'])) {
241
            $config['cancel_url'] = $paymentsConfig['cancel_url'];
242
        }
243
244
        return $config;
245
    }
246
247
    protected function handleAdyen(array $paymentsConfig, ContainerBuilder $containerBuilder): array
248
    {
249
        if (!isset($paymentsConfig['adyen']['api_key'])) {
250
            throw new ParameterNotSetException('billing.payments.adyen.api_key must be set.');
251
        }
252
        if (!isset($paymentsConfig['adyen']['merchant_account'])) {
253
            throw new ParameterNotSetException('billing.payments.adyen.merchant_account must be set.');
254
        }
255
256
        $pciMode = false;
257
        if (isset($paymentsConfig['pci_mode'])) {
258
            $pciMode = $paymentsConfig['pci_mode'];
259
        }
260
261
        $testMode = true;
262
        if (isset($paymentsConfig['adyen']['test_mode'])) {
263
            $testMode = $paymentsConfig['adyen']['test_mode'];
264
        }
265
266
        $config = [
267
            'provider' => 'adyen',
268
            'api_key' => $paymentsConfig['adyen']['api_key'],
269
            'merchant_account' => $paymentsConfig['adyen']['merchant_account'],
270
            'pci_mode' => $pciMode,
271
            'test_mode' => $testMode,
272
        ];
273
274
        if ($paymentsConfig['adyen']['prefix']) {
275
            $config['prefix'] = $paymentsConfig['adyen']['prefix'];
276
        }
277
278
        if (isset($paymentsConfig['return_url'])) {
279
            $config['return_url'] = $paymentsConfig['return_url'];
280
        }
281
282
        $containerBuilder->setParameter('parthenon_billing_config_frontend_info', $paymentsConfig['adyen']['cse_url']);
283
        $containerBuilder->setParameter('parthenon_billing_config_webhook_secret', $paymentsConfig['adyen']['webhook_secret'] ?? '');
284
285
        return $config;
286
    }
287
288
    private function getPlansNode(): NodeDefinition
289
    {
290
        $treeBuilder = new TreeBuilder('plan');
291
        $node = $treeBuilder->getRootNode();
292
293
        /** @var ArrayNodeDefinition $planNode */
294
        $planNode = $node
295
            ->requiresAtLeastOneElement()
296
            ->useAttributeAsKey('name')
297
            ->prototype('array');
298
299
        $planNode
300
            ->fixXmlConfig('limits')
301
                ->children()
302
                    ->booleanNode('is_free')->defaultFalse()->end()
303
                    ?->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

303
                    ?->/** @scrutinizer ignore-call */ booleanNode('is_per_seat')->defaultFalse()->end()
Loading history...
304
                    ?->booleanNode('public')->defaultTrue()->end()
305
                    ?->booleanNode('has_trial')->defaultFalse()->end()
306
                    ?->scalarNode('trial_length_days')->defaultValue(0)->end()
307
                    ?->scalarNode('user_count')->end()
308
                    ->arrayNode('features')
309
                        ->scalarPrototype()->end()
310
                    ->end()
311
                    ->arrayNode('limit')
312
                        ->useAttributeAsKey('name')
313
                        ->prototype('array')
314
                        ->children()
315
                            ->integerNode('limit')->end()
316
                            ->scalarNode('description')->end()
317
                        ->end()
318
                    ->end()
319
                ->end()
320
            ->append($this->buildPricesNode())
321
            ->end();
322
323
        return $node;
324
    }
325
}
326