-
Notifications
You must be signed in to change notification settings - Fork 19
Feature/pre 3469 #305
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
Draft
jhoaraupp
wants to merge
11
commits into
develop
Choose a base branch
from
feature/PRE-3469
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Feature/pre 3469 #305
Changes from 1 commit
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
690d9f5
PRE-3469: Validate contracts with Sylius
jhoaraupp b97c69d
PRE-3469: Promote unified-plugin-core to a real dependency
jhoaraupp 29b4b8d
PRE-3469: Remove design spec accidentally committed in b97c69d
jhoaraupp e6ad438
PRE-3469: Promote SyliusOrderStateMutator to PayplugOrderStateMutator
jhoaraupp 463c79c
PRE-3469: Promote SyliusTokenCache to PayplugTokenCache
jhoaraupp 753ded3
PRE-3469: Promote SyliusConfigurationRepository to PayplugConfigurati…
jhoaraupp d770af8
PRE-3469: Wire PayplugOrderStateMutator into NotifyPaymentRequestHandler
jhoaraupp 1a89a34
PRE-3469: Fix phpstan error in NotifyPaymentRequestHandler's mutator …
jhoaraupp ddb7742
PRE-3469: Update VALIDATION.md for the promoted contracts
jhoaraupp f0cd2a8
PRE-3469: Add explicit service alias for IOrderStateMutator
jhoaraupp 3ded48e
PRE-3469: Guard PayplugOrderStateMutator call against multi-payment o…
jhoaraupp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace PayPlug\SyliusPayPlugPlugin\Migrations; | ||
|
|
||
| use Doctrine\DBAL\Schema\Schema; | ||
| use Doctrine\Migrations\AbstractMigration; | ||
|
|
||
| /** | ||
| * PRE-3469 spike only: schema for the throwaway PayplugOperation entity (src/Spike/Entity). | ||
| * A real migration — not a manual step or a SchemaTool call from the test — because the | ||
| * mapping is registered whenever kernel.environment=test (see | ||
| * PayPlugSyliusPayPlugExtension::prependSpikeDoctrineMapping()), which includes routine | ||
| * `sylius:fixtures:load` runs on a fresh checkout, not just the spike's own integration test. | ||
| * Without this migration, `sylius:fixtures:load` fails for anyone setting up the test | ||
| * application from scratch — found by testing this on a clean database, not by reasoning about | ||
| * it. Guarded to APP_ENV=test in both directions so a real merchant deployment never gets this | ||
| * table created (up) or, if it somehow did, never gets it dropped outside test either (down) — | ||
| * a normal plugin migration otherwise runs unconditionally, including in production. Drop this | ||
| * migration together with src/Spike/ once the spike is closed. | ||
| */ | ||
| final class Version20260720100000 extends AbstractMigration | ||
| { | ||
| public function getDescription(): string | ||
| { | ||
| return 'PRE-3469 spike: add payplug_operation table for the throwaway PayplugOperation entity.'; | ||
| } | ||
|
|
||
| public function up(Schema $schema): void | ||
| { | ||
| $this->skipIf( | ||
| 'test' !== ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null), | ||
| 'PRE-3469 spike-only schema, not applied outside the test environment.', | ||
| ); | ||
|
|
||
| $this->addSql('CREATE TABLE payplug_operation ( | ||
| id INT AUTO_INCREMENT NOT NULL, | ||
| operation_id VARCHAR(255) NOT NULL, | ||
| exec_code VARCHAR(255) NOT NULL, | ||
| outcome VARCHAR(255) NOT NULL, | ||
| amount INT NOT NULL, | ||
| order_id VARCHAR(255) NOT NULL, | ||
| treated TINYINT(1) NOT NULL, | ||
| treated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', | ||
| created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', | ||
| UNIQUE INDEX payplug_operation_id_unique (operation_id), | ||
| INDEX payplug_operation_order_id_idx (order_id), | ||
| PRIMARY KEY(id) | ||
| ) DEFAULT CHARACTER SET UTF8 COLLATE `UTF8_unicode_ci` ENGINE = InnoDB'); | ||
| } | ||
|
jhoaraupp marked this conversation as resolved.
|
||
|
|
||
| public function down(Schema $schema): void | ||
| { | ||
| $this->skipIf( | ||
| 'test' !== ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null), | ||
| 'PRE-3469 spike-only schema, not applied outside the test environment.', | ||
| ); | ||
|
|
||
| $this->addSql('DROP TABLE payplug_operation'); | ||
| } | ||
| } | ||
|
jhoaraupp marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace PayPlug\SyliusPayPlugPlugin\Spike\Entity; | ||
|
|
||
| use Doctrine\DBAL\Types\Types; | ||
| use Doctrine\ORM\Mapping as ORM; | ||
| use PayplugUnifiedCore\Models\OperationData; | ||
|
|
||
| /** | ||
| * PRE-3469 spike: normalized Doctrine schema for OperationData — not shipped code. | ||
| * | ||
| * Friction found: the plugin currently stores the Payplug payment id inline in Sylius's own | ||
| * Payment::details JSON blob and looks it up with a `LIKE '%id%'` query (see | ||
| * PaymentRepository::findOneByPayPlugPaymentId) — there is no separate "operation" table today. | ||
| * That works for the single id lookup it was built for, but it cannot support | ||
| * IPaymentRepository::markTreated()/isTreated() (needs its own indexed idempotency flag) or | ||
| * getByOperationId() (needs operationId to be a real, indexed column, not a substring match | ||
| * inside a serialized blob). Hence this new table rather than extending the existing one. | ||
| * | ||
| * Separately, this class living outside src/Entity/ means it is not auto-registered the way | ||
| * this plugin's other entities are (see config/resources.yaml, which wires Card and | ||
| * RefundHistory as `sylius_resource` entries) — a real (non-spike) version of this table would | ||
| * need either a `sylius_resource` entry or an explicit `doctrine.orm.mappings` prepend for | ||
| * whatever directory it lives in. | ||
| */ | ||
| #[ORM\Entity] | ||
| #[ORM\Table(name: 'payplug_operation')] | ||
| #[ORM\UniqueConstraint(name: 'payplug_operation_id_unique', columns: ['operation_id'])] | ||
| #[ORM\Index(name: 'payplug_operation_order_id_idx', columns: ['order_id'])] | ||
| class PayplugOperation | ||
| { | ||
| #[ORM\Id] | ||
| #[ORM\GeneratedValue] | ||
| #[ORM\Column(type: Types::INTEGER)] | ||
| private ?int $id = null; | ||
|
|
||
| #[ORM\Column(name: 'operation_id', type: Types::STRING)] | ||
| private string $operationId; | ||
|
|
||
| #[ORM\Column(name: 'exec_code', type: Types::STRING)] | ||
| private string $execCode; | ||
|
|
||
| #[ORM\Column(name: 'outcome', type: Types::STRING)] | ||
| private string $outcome; | ||
|
|
||
| #[ORM\Column(name: 'amount', type: Types::INTEGER)] | ||
| private int $amount; | ||
|
|
||
| #[ORM\Column(name: 'order_id', type: Types::STRING)] | ||
| private string $orderId; | ||
|
|
||
| #[ORM\Column(name: 'treated', type: Types::BOOLEAN)] | ||
| private bool $treated = false; | ||
|
|
||
| #[ORM\Column(name: 'treated_at', type: Types::DATETIME_IMMUTABLE, nullable: true)] | ||
| private ?\DateTimeImmutable $treatedAt = null; | ||
|
|
||
| #[ORM\Column(name: 'created_at', type: Types::DATETIME_IMMUTABLE)] | ||
| private \DateTimeImmutable $createdAt; | ||
|
|
||
| public function __construct(string $operationId, string $execCode, string $outcome, int $amount, string $orderId) | ||
| { | ||
| $this->operationId = $operationId; | ||
| $this->execCode = $execCode; | ||
| $this->outcome = $outcome; | ||
| $this->amount = $amount; | ||
| $this->orderId = $orderId; | ||
| $this->createdAt = new \DateTimeImmutable(); | ||
| } | ||
|
|
||
| public static function fromOperationData(OperationData $operationData): self | ||
| { | ||
| return new self( | ||
| $operationData->operationId, | ||
| $operationData->execCode, | ||
| $operationData->outcome, | ||
| $operationData->amount, | ||
| $operationData->orderId, | ||
| ); | ||
| } | ||
|
|
||
| public function updateFromOperationData(OperationData $operationData): void | ||
| { | ||
| $this->execCode = $operationData->execCode; | ||
| $this->outcome = $operationData->outcome; | ||
| $this->amount = $operationData->amount; | ||
| } | ||
|
|
||
| public function toOperationData(): OperationData | ||
| { | ||
| return new OperationData($this->operationId, $this->execCode, $this->outcome, $this->amount, $this->orderId); | ||
| } | ||
|
|
||
| public function getOperationId(): string | ||
| { | ||
| return $this->operationId; | ||
| } | ||
|
|
||
| public function getOrderId(): string | ||
| { | ||
| return $this->orderId; | ||
| } | ||
|
|
||
| public function isTreated(): bool | ||
| { | ||
| return $this->treated; | ||
| } | ||
|
|
||
| public function markTreated(): void | ||
| { | ||
| $this->treated = true; | ||
| $this->treatedAt = new \DateTimeImmutable(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace PayPlug\SyliusPayPlugPlugin\Spike; | ||
|
|
||
| use PayplugUnifiedCore\Contracts\IConfigurationRepository; | ||
| use PayplugUnifiedCore\Exceptions\ApiException; | ||
| use Sylius\Component\Payment\Model\GatewayConfigInterface; | ||
|
|
||
| /** | ||
| * PRE-3469 spike: proof-of-concept implementation of IConfigurationRepository against Sylius's | ||
| * GatewayConfigInterface — not shipped code. | ||
| * | ||
| * Friction found: IConfigurationRepository assumes one flat set of credentials, but Sylius | ||
| * scopes gateway config per PaymentMethod *and* per live/test mode (`config['live_client']` vs | ||
| * `config['test_client']`, selected by `config['live']`, exactly as PayPlugApiClientFactory | ||
| * already does). So a single IConfigurationRepository instance has to be constructed per | ||
| * GatewayConfigInterface (i.e. per PaymentMethod) rather than shared as one repository-wide | ||
| * service — a factory, not a singleton. Not blocking, but worth flagging if the Unified API | ||
| * client this feeds ever assumes one repository == one merchant. | ||
| * | ||
| * Positive finding: Sylius already ships an (experimental) GatewayConfigEncrypter that | ||
| * transparently encrypts the whole `getConfig()` array at rest | ||
| * (Sylius\Component\Payment\Encryption) — if wired up, CLIENT_SECRET benefits from that for | ||
| * free. What this class must still guarantee on its own is that a *decrypted* secret never | ||
| * leaks into a log line or exception message, which is why requireString() below only ever | ||
| * interpolates the config *key name*, never its value. | ||
| */ | ||
| final class SyliusConfigurationRepository implements IConfigurationRepository | ||
| { | ||
| public function __construct(private readonly GatewayConfigInterface $gatewayConfig) | ||
| { | ||
| } | ||
|
|
||
| public function get(string $key): ?string | ||
| { | ||
| $client = $this->activeClientConfig(); | ||
| $value = $client[$key] ?? null; | ||
|
|
||
| return \is_string($value) ? $value : null; | ||
| } | ||
|
|
||
| public function set(string $key, string $value): void | ||
| { | ||
| $config = $this->gatewayConfig->getConfig(); | ||
| $scope = $this->activeScope($config); | ||
| $client = $config[$scope] ?? []; | ||
| if (!\is_array($client)) { | ||
| $client = []; | ||
| } | ||
| $client[$key] = $value; | ||
| $config[$scope] = $client; | ||
|
|
||
| // Persisting $config is the caller's responsibility (Doctrine flush), same as every | ||
| // other GatewayConfigInterface mutation in this plugin (see UnifiedAuthenticationController). | ||
| $this->gatewayConfig->setConfig($config); | ||
| } | ||
|
|
||
| public function getClientId(): string | ||
| { | ||
| return $this->requireString('client_id'); | ||
| } | ||
|
|
||
| public function getClientSecret(): string | ||
| { | ||
| return $this->requireString('client_secret'); | ||
| } | ||
|
|
||
| public function getPublicKeyId(): string | ||
| { | ||
| return $this->requireString('public_key_id'); | ||
| } | ||
|
|
||
| public function getPublicKeyValue(): string | ||
| { | ||
| return $this->requireString('public_key_value'); | ||
| } | ||
|
|
||
| private function requireString(string $key): string | ||
| { | ||
| $value = $this->get($key); | ||
| if (null === $value || '' === $value) { | ||
| // Never interpolate the resolved *value* here, only the key name and factory name. | ||
| throw new ApiException(\sprintf( | ||
| 'Missing "%s" in gateway configuration "%s".', | ||
| $key, | ||
| $this->gatewayConfig->getFactoryName() ?? 'unknown', | ||
| )); | ||
| } | ||
|
|
||
| return $value; | ||
| } | ||
|
|
||
| /** | ||
| * @return array<string, mixed> | ||
| */ | ||
| private function activeClientConfig(): array | ||
| { | ||
| $config = $this->gatewayConfig->getConfig(); | ||
| $client = $config[$this->activeScope($config)] ?? []; | ||
| if (!\is_array($client)) { | ||
| return []; | ||
| } | ||
|
|
||
| /** @var array<string, mixed> $typedClient */ | ||
| $typedClient = $client; | ||
|
|
||
| return $typedClient; | ||
| } | ||
|
|
||
| /** | ||
| * @param array<string, mixed> $config | ||
| */ | ||
| private function activeScope(array $config): string | ||
| { | ||
| return true === ($config['live'] ?? false) ? 'live_client' : 'test_client'; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.