Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 8 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,28 @@
"test": [
"@test:core",
"@test:tools",
"@test:hello-module"
"@test:hello-module",
"@test:shop-module"
],
"test:core": "phpunit --configuration packages/cms-core/phpunit.xml --testsuite cms-core",
"test:tools": "phpunit --configuration tools/phpunit.xml",
"test:hello-module": "phpunit --configuration packages/hello-module/phpunit.xml"
"test:hello-module": "phpunit --configuration packages/hello-module/phpunit.xml",
"test:shop-module": "phpunit --configuration packages/shop-module/phpunit.xml"
},
"autoload": {
"psr-4": {
"Easeo\\Cms\\": "packages/cms-core/src/",
"Easeo\\Hello\\": "packages/hello-module/src/"
"Easeo\\Hello\\": "packages/hello-module/src/",
"Easeo\\Shop\\": "packages/shop-module/src/"
}
},
"autoload-dev": {
"psr-4": {
"Easeo\\Tools\\": "tools/src/",
"Easeo\\Tools\\Tests\\": "tools/tests/",
"Easeo\\Cms\\Tests\\": "packages/cms-core/tests/",
"Easeo\\Hello\\Tests\\": "packages/hello-module/tests/"
"Easeo\\Hello\\Tests\\": "packages/hello-module/tests/",
"Easeo\\Shop\\Tests\\": "packages/shop-module/tests/"
}
}
}
7 changes: 7 additions & 0 deletions packages/cms-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ Versionering: [SemVer](https://semver.org/lang/nl/).

## [Unreleased]

### Added — Plan 03 batch 6 (shop-module skeleton)
- **packages/shop-module/** — Shop-module skelet. Registreert alleen een geparkeerde beheer-tab. Payment-provider abstractie beschikbaar via `Easeo\Shop\Payment\PaymentProviderFactory::make()`.
- `Easeo\Shop\Payment\{PaymentProvider, PaymentStatus, RefundResult, NotImplementedException}` — public API.
- 3 stub adapters: `MollieProvider`, `StripeProvider`, `AdyenProvider` — alle methods werpen `NotImplementedException`. Echte implementaties komen bij eurolight-webshop kickoff.
- `PaymentProviderFactory` leest `PAYMENT_PROVIDER` env + bijbehorende API-key env. Throws RuntimeException als config ontbreekt.
- ~20 nieuwe tests via dataProvider-style stub-coverage.

### Added — Plan 03 batches 3+4+5 (router-integration + nav-merge + hello-module)
- `apps/easeo-website/public/module-router.php` — fallback entry that dispatches module-registered routes via `App::registry()->routes()->match()`. Boots App if not already booted; 404-safe on no match, 500-safe on handler exception.
- `apps/easeo-website/public/beheer-entry.php` — beheer-UI entry-point that calls `App::boot()` before including `cms-core/beheer/index.php`, ensuring modules are registered before the admin UI renders.
Expand Down
3 changes: 3 additions & 0 deletions packages/shop-module/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
vendor/
.phpunit.cache/
composer.lock
33 changes: 33 additions & 0 deletions packages/shop-module/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# easeo/shop-module

EASEO CMS — Shop module skeleton.

> **Skelet status.** Echte shop-logica (cart, checkout, orders, product-catalog) komt bij de eurolight-webshop kickoff. Op dit moment registreert deze module alleen een geparkeerde beheer-tab en biedt een Payment-provider abstractie met stub adapters.

## Wat zit er nu wel in?

- `Easeo\Shop\Module` — registreert een admin-only beheer-tab "Shop" met een "geparkeerd" placeholder
- `Easeo\Shop\Payment\PaymentProvider` interface
- `Easeo\Shop\Payment\PaymentStatus` enum (Pending/Paid/Failed/Expired/Refunded/Cancelled)
- `Easeo\Shop\Payment\RefundResult` value-object
- `Easeo\Shop\Payment\PaymentProviderFactory::make()` — leest `PAYMENT_PROVIDER` env var en instantieert juiste adapter
- 3 stub adapters: `MollieProvider`, `StripeProvider`, `AdyenProvider` — alle methods werpen `NotImplementedException`

## Configuratie

In `.env`:

```
PAYMENT_PROVIDER=mollie # of stripe / adyen
MOLLIE_API_KEY=test_xxx # alleen vereist als PAYMENT_PROVIDER=mollie
STRIPE_SECRET_KEY=sk_xxx # alleen vereist als PAYMENT_PROVIDER=stripe
ADYEN_API_KEY=AQE... # alleen vereist als PAYMENT_PROVIDER=adyen
```

## Wat komt later (eurolight-kickoff)

- Echte adapter-implementaties (Mollie/Stripe/Adyen API-calls met signature verification)
- Cart engine + product-catalog
- Order-management + admin
- Webhook handlers per provider
- Email-notificaties + invoice-PDFs (via `easeo/cms-core` Mailer)
31 changes: 31 additions & 0 deletions packages/shop-module/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "easeo/shop-module",
"description": "EASEO CMS — Shop module skeleton with Payment-provider abstraction. Skelet only; eurolight-webshop logic comes later.",
"type": "library",
"license": "MIT",
"require": {
"php": ">=8.2",
"easeo/cms-core": "*"
},
"require-dev": {
"phpunit/phpunit": "^10.5"
},
"autoload": {
"psr-4": {
"Easeo\\Shop\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Easeo\\Shop\\Tests\\": "tests/"
}
},
"extra": {
"easeo": {
"module-class": "Easeo\\Shop\\Module"
}
},
"config": {
"sort-packages": true
}
}
17 changes: 17 additions & 0 deletions packages/shop-module/phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="shop-module">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
44 changes: 44 additions & 0 deletions packages/shop-module/src/Module.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);

namespace Easeo\Shop;

use Easeo\Cms\Module\ModuleInterface;
use Easeo\Cms\Module\Registry;

/**
* Shop-module skelet. Registers only a placeholder beheer-tab.
* Echte shop-logica (cart, checkout, orders) komt bij de eurolight-
* webshop kickoff — zie `packages/shop-module/README.md`.
*/
final class Module implements ModuleInterface
{
public function name(): string
{
return 'easeo/shop-module';
}

public function version(): string
{
return '0.1.0';
}

public function register(Registry $r): void
{
$r->beheerPage(
'shop',
'Shop',
// Shopping cart icon (Heroicons outline)
'M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z',
function (): string {
return '<div class="admin-card">'
. '<h2 class="text-xl font-semibold mb-2">Shop module — skelet</h2>'
. '<p class="text-gray-400">Deze module is geparkeerd. Echte cart/checkout/orders logica komt bij de eurolight-webshop kickoff.</p>'
. '<p class="text-sm text-gray-500 mt-3">Payment-provider abstractie is wel beschikbaar: <code>Easeo\\Shop\\Payment\\PaymentProviderFactory</code>. Configureer <code>PAYMENT_PROVIDER</code> in .env.</p>'
. '</div>';
},
$this->name(),
true // admin-only
);
}
}
15 changes: 15 additions & 0 deletions packages/shop-module/src/Payment/NotImplementedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);

namespace Easeo\Shop\Payment;

class NotImplementedException extends \RuntimeException
{
public function __construct(string $method)
{
parent::__construct(
"Payment provider method '$method' is not implemented in the skeleton. "
. "Implement when eurolight-webshop kickoff starts — see packages/shop-module/README.md."
);
}
}
47 changes: 47 additions & 0 deletions packages/shop-module/src/Payment/PaymentProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);

namespace Easeo\Shop\Payment;

/**
* Contract for payment providers. Implementations live under
* src/Payment/Providers/ (MollieProvider, StripeProvider, AdyenProvider).
*
* Skeleton implementations throw NotImplementedException — real logic
* comes with the eurolight-webshop project.
*
* All amounts are in cents (int) to avoid float arithmetic on money.
* All identifiers are provider-side strings (Mollie's payment id,
* Stripe's PaymentIntent id, etc.) — the shop-module wraps them in
* its own order-id mapping (later batch).
*/
interface PaymentProvider
{
public function name(): string;

/**
* Create a new payment. Returns the provider's checkout URL the
* customer should be redirected to.
*
* @param array<string,mixed> $metadata Provider-side metadata (order-id, customer-id, etc.)
* @return array{paymentId:string, checkoutUrl:string}
*/
public function createPayment(int $amountCents, string $currency, string $description, string $webhookUrl, string $returnUrl, array $metadata = []): array;

/**
* Verify a webhook delivery. Providers send a POST with an id; this
* method confirms authenticity (signature check) and fetches the
* current status from the provider's API.
*/
public function verifyWebhook(array $headers, string $rawBody): PaymentStatus;

/**
* Fetch the current status of an existing payment.
*/
public function fetchStatus(string $paymentId): PaymentStatus;

/**
* Refund a (partially or fully) paid payment.
*/
public function refund(string $paymentId, int $amountCents, ?string $reason = null): RefundResult;
}
50 changes: 50 additions & 0 deletions packages/shop-module/src/Payment/PaymentProviderFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);

namespace Easeo\Shop\Payment;

use Easeo\Cms\Config\Environment;
use Easeo\Shop\Payment\Providers\AdyenProvider;
use Easeo\Shop\Payment\Providers\MollieProvider;
use Easeo\Shop\Payment\Providers\StripeProvider;

final class PaymentProviderFactory
{
private const PROVIDERS = ['mollie', 'stripe', 'adyen'];

public static function make(?string $providerName = null): PaymentProvider
{
$name = $providerName ?? Environment::get('PAYMENT_PROVIDER', '');
if ($name === null || $name === '') {
throw new \RuntimeException(
"PAYMENT_PROVIDER is niet ingesteld in .env. Geldig: " . implode(', ', self::PROVIDERS)
);
}
$name = strtolower($name);

return match ($name) {
'mollie' => new MollieProvider(self::requireKey('MOLLIE_API_KEY')),
'stripe' => new StripeProvider(self::requireKey('STRIPE_SECRET_KEY')),
'adyen' => new AdyenProvider(self::requireKey('ADYEN_API_KEY')),
default => throw new \InvalidArgumentException(
"Onbekende PAYMENT_PROVIDER '$name'. Geldig: " . implode(', ', self::PROVIDERS)
),
};
}

public static function knownProviders(): array
{
return self::PROVIDERS;
}

private static function requireKey(string $envVar): string
{
$value = Environment::get($envVar, '');
if ($value === null || $value === '') {
throw new \RuntimeException(
"Payment provider vereist $envVar in .env (niet ingesteld of leeg)"
);
}
return $value;
}
}
14 changes: 14 additions & 0 deletions packages/shop-module/src/Payment/PaymentStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);

namespace Easeo\Shop\Payment;

enum PaymentStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Failed = 'failed';
case Expired = 'expired';
case Refunded = 'refunded';
case Cancelled = 'cancelled';
}
36 changes: 36 additions & 0 deletions packages/shop-module/src/Payment/Providers/AdyenProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);

namespace Easeo\Shop\Payment\Providers;

use Easeo\Shop\Payment\NotImplementedException;
use Easeo\Shop\Payment\PaymentProvider;
use Easeo\Shop\Payment\PaymentStatus;
use Easeo\Shop\Payment\RefundResult;

final class AdyenProvider implements PaymentProvider
{
public function __construct(private readonly string $apiKey) {}

public function name(): string { return 'adyen'; }

public function createPayment(int $amountCents, string $currency, string $description, string $webhookUrl, string $returnUrl, array $metadata = []): array
{
throw new NotImplementedException(self::class . '::createPayment');
}

public function verifyWebhook(array $headers, string $rawBody): PaymentStatus
{
throw new NotImplementedException(self::class . '::verifyWebhook');
}

public function fetchStatus(string $paymentId): PaymentStatus
{
throw new NotImplementedException(self::class . '::fetchStatus');
}

public function refund(string $paymentId, int $amountCents, ?string $reason = null): RefundResult
{
throw new NotImplementedException(self::class . '::refund');
}
}
36 changes: 36 additions & 0 deletions packages/shop-module/src/Payment/Providers/MollieProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);

namespace Easeo\Shop\Payment\Providers;

use Easeo\Shop\Payment\NotImplementedException;
use Easeo\Shop\Payment\PaymentProvider;
use Easeo\Shop\Payment\PaymentStatus;
use Easeo\Shop\Payment\RefundResult;

final class MollieProvider implements PaymentProvider
{
public function __construct(private readonly string $apiKey) {}

public function name(): string { return 'mollie'; }

public function createPayment(int $amountCents, string $currency, string $description, string $webhookUrl, string $returnUrl, array $metadata = []): array
{
throw new NotImplementedException(self::class . '::createPayment');
}

public function verifyWebhook(array $headers, string $rawBody): PaymentStatus
{
throw new NotImplementedException(self::class . '::verifyWebhook');
}

public function fetchStatus(string $paymentId): PaymentStatus
{
throw new NotImplementedException(self::class . '::fetchStatus');
}

public function refund(string $paymentId, int $amountCents, ?string $reason = null): RefundResult
{
throw new NotImplementedException(self::class . '::refund');
}
}
Loading
Loading