PHPackages                             payroad/payroad-core - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. payroad/payroad-core

ActiveLibrary[Payment Processing](/categories/payments)

payroad/payroad-core
====================

Payment domain core — aggregates, ports, and application use cases for Payroad

80PHPCI passing

Since Apr 4Pushed 2mo agoCompare

[ Source](https://github.com/payroad/payroad-core)[ Packagist](https://packagist.org/packages/payroad/payroad-core)[ RSS](/packages/payroad-payroad-core/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependenciesVersions (1)Used By (0)

payroad-core
============

[](#payroad-core)

 [![Tests](https://github.com/payroad/payroad-core/workflows/Tests/badge.svg)](https://github.com/payroad/payroad-core/actions) [![Latest Version](https://camo.githubusercontent.com/6503bcc5cd4e1af32205e9c960561e9890b99448e9a226b2fd284de389595ad0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f706179726f61642f706179726f61642d636f7265)](https://packagist.org/packages/payroad/payroad-core) [![PHP Version](https://camo.githubusercontent.com/ddd610d3ccf43f9277f2be1d5764b23f6e90edc0555a9374f2e8c02baa0f9afe/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f706179726f61642f706179726f61642d636f7265)](https://packagist.org/packages/payroad/payroad-core) [![License](https://camo.githubusercontent.com/ec3e29f4536e89fdafa968a5ec9c6eb3ce8db2527e5503c4af5ed188a6360b9c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f706179726f61642f706179726f61642d636f7265)](https://github.com/payroad/payroad-core/blob/main/LICENSE)

Framework-agnostic payment domain for the Payroad ecosystem — aggregates, use cases, and provider ports with no framework or database dependencies.

---

Installation
------------

[](#installation)

```
composer require payroad/payroad-core
```

Requires PHP 8.2+.

---

Overview
--------

[](#overview)

`payroad-core` is the shared kernel of the Payroad payment platform. It defines the domain model and the contracts (ports) that infrastructure must implement.

```
┌──────────────────────────────────┐
│         Your Application         │
│   (Symfony / Laravel / custom)   │
└────────────────┬─────────────────┘
                 │ uses
┌────────────────▼─────────────────┐
│           payroad-core           │
│  Domain · Use Cases · Ports      │
└───────┬──────────────┬───────────┘
        │ implements   │ implements
┌───────▼──────┐ ┌─────▼────────────┐
│   Provider   │ │  Infrastructure  │
│ stripe, etc. │ │  DB, Events, …   │
└──────────────┘ └──────────────────┘

```

This package contains **no** HTTP, ORM, or provider-specific code.

---

Payment flows
-------------

[](#payment-flows)

Four payment methods are supported, each with its own aggregate, state machine, and refund flow:

FlowAttempt classKey steps**Card**`CardPaymentAttempt`Authorize → (3DS) → Capture / Void**Crypto**`CryptoPaymentAttempt`Address issued → Confirmations → Settled**P2P**`P2PPaymentAttempt`QR / redirect → User confirms → Settled**Cash**`CashPaymentAttempt`Voucher issued → Cash collected → Settled---

Usage
-----

[](#usage)

### Create a payment

[](#create-a-payment)

```
use Payroad\Application\UseCase\Payment\CreatePaymentCommand;
use Payroad\Domain\Money\Currency;
use Payroad\Domain\Money\Money;
use Payroad\Domain\Payment\CustomerId;
use Payroad\Domain\Payment\PaymentMetadata;

$command = new CreatePaymentCommand(
    amount:    Money::ofDecimal('49.99', new Currency('USD', 2)),
    customerId: CustomerId::of('customer-456'),
    metadata:  PaymentMetadata::fromArray(['orderId' => '789']),
    expiresAt: new DateTimeImmutable('+30 minutes'),
);

$useCase->execute($command);
```

Supply your own ID when needed (e.g. client-generated UUID):

```
use Payroad\Domain\Payment\PaymentId;

$command = new CreatePaymentCommand(
    // ...
    id: PaymentId::fromUuid('018e4c3d-1a2b-7000-...'),
);
```

### Initiate a card attempt

[](#initiate-a-card-attempt)

```
use Payroad\Application\UseCase\Card\InitiateCardAttemptCommand;
use Payroad\Port\Provider\Card\CardAttemptContext;

$command = new InitiateCardAttemptCommand(
    paymentId:    $payment->getId(),
    providerName: 'stripe',
    context:      new CardAttemptContext(ip: '1.2.3.4', userAgent: '...'),
);

$useCase->execute($command);
```

### Capture / void an authorized card

[](#capture--void-an-authorized-card)

```
use Payroad\Application\UseCase\Card\CaptureCardAttemptCommand;
use Payroad\Application\UseCase\Card\VoidCardAttemptCommand;

// Full capture
$captureUseCase->execute(new CaptureCardAttemptCommand($attempt->getId()));

// Partial capture
$captureUseCase->execute(new CaptureCardAttemptCommand(
    $attempt->getId(),
    Money::ofDecimal('25.00', new Currency('USD', 2)),
));

// Void (release the authorization)
$voidUseCase->execute(new VoidCardAttemptCommand($attempt->getId()));
```

### Handle an incoming webhook

[](#handle-an-incoming-webhook)

```
use Payroad\Application\UseCase\Webhook\HandleWebhookCommand;

$useCase->execute(new HandleWebhookCommand(
    providerName: 'stripe',
    payload:      $request->toArray(),
    headers:      $request->headers->all(),
));
```

The use case is idempotent — duplicate webhooks from at-least-once providers are safely ignored.

### Cancel or expire a payment

[](#cancel-or-expire-a-payment)

```
use Payroad\Application\UseCase\Payment\CancelPaymentCommand;
use Payroad\Application\UseCase\Payment\ExpirePaymentCommand;

$cancelUseCase->execute(new CancelPaymentCommand($payment->getId()));
$expireUseCase->execute(new ExpirePaymentCommand($payment->getId()));
```

---

Key design decisions
--------------------

[](#key-design-decisions)

### Payment and PaymentAttempt are separate aggregates

[](#payment-and-paymentattempt-are-separate-aggregates)

`Payment` is a thin business document — amount, customer, status. It never holds attempt objects, only the ID of the winning attempt once resolved.

`PaymentAttempt` is the operational aggregate that drives actual money movement through a provider. Multiple attempts may exist per payment (retries).

### Money carries precision explicitly

[](#money-carries-precision-explicitly)

```
// Fiat — precision from ISO 4217, provided by infrastructure KnownCurrencies
new Currency('USD', 2)   // 1 USD = 100 cents
new Currency('JPY', 0)   // 1 JPY, no subunits
new Currency('KWD', 3)   // 1 KWD = 1000 fils

// Crypto — precision always explicit
new Currency('BTC',  8)  // 1 BTC = 100_000_000 satoshis
new Currency('USDT', 6)  // 1 USDT = 1_000_000 micro-USDT
new Currency('ETH',  18) // ⚠ int overflow above ~9.2 ETH — use ofDecimal()

Money::ofMinor(4999, new Currency('USD', 2))           // $49.99
Money::ofDecimal('0.00100000', new Currency('BTC', 8)) // 100 000 satoshis
```

`Currency` carries no registry — precision is resolved by the infrastructure layer before entering the domain.

### State machines are embedded per flow

[](#state-machines-are-embedded-per-flow)

Each attempt subclass validates transitions before applying them:

```
Card:    PENDING → AWAITING_CONFIRMATION → AUTHORIZED → PROCESSING → SUCCEEDED
                 └──────────────────────→             └→ FAILED
                                                       └→ EXPIRED

Crypto:  PENDING → PROCESSING → SUCCEEDED | FAILED | EXPIRED

P2P:     PENDING → AWAITING_CONFIRMATION → PROCESSING → SUCCEEDED | FAILED | EXPIRED

Cash:    PENDING → AWAITING_CONFIRMATION → SUCCEEDED | FAILED | EXPIRED

```

An invalid transition throws `\DomainException` before any state changes.

### Domain events

[](#domain-events)

Every state change produces typed events consumed by your application layer:

EventTrigger`PaymentCreated`Payment created`PaymentProcessingStarted`First attempt initiated`PaymentSucceeded`Attempt settled`PaymentRetryAvailable`Attempt failed, payment re-queued for retry`PaymentCanceled` / `PaymentExpired` / `PaymentFailed`Terminal transitions`AttemptInitiated`Attempt created`AttemptAuthorized`Card authorized, ready to capture`AttemptRequiresUserAction`3DS / P2P redirect needed`AttemptSucceeded` / `AttemptFailed` / `AttemptCanceled` / `AttemptExpired`Terminal attempt states---

Implementing a provider
-----------------------

[](#implementing-a-provider)

See **[docs/writing-a-provider.md](docs/writing-a-provider.md)** for a complete step-by-step guide covering data classes, provider implementation, factory, Symfony registration, and tests.

Quick overview — create a Composer package and implement the port interface for your payment method:

```
use Payroad\Domain\Attempt\AttemptStatus;
use Payroad\Domain\Attempt\PaymentAttemptId;
use Payroad\Domain\Money\Money;
use Payroad\Domain\Payment\PaymentId;
use Payroad\Domain\Channel\Card\CardPaymentAttempt;
use Payroad\Port\Provider\Card\CardAttemptContext;
use Payroad\Port\Provider\Card\CardProviderInterface;
use Payroad\Port\Provider\Card\CaptureResult;
use Payroad\Port\Provider\WebhookResult;

final class StripeCardProvider implements CardProviderInterface
{
    public function name(): string
    {
        return 'stripe';
    }

    public function initiateCardAttempt(
        PaymentAttemptId   $id,
        PaymentId          $paymentId,
        string             $providerName,
        Money              $amount,
        CardAttemptContext $context,
    ): CardPaymentAttempt {
        $intent = $this->stripe->paymentIntents->create([
            'amount'   => $amount->getMinorAmount(),
            'currency' => strtolower((string) $amount->getCurrency()),
        ]);

        $attempt = CardPaymentAttempt::create(
            $id, $paymentId, $providerName, $amount,
            new StripeCardData(clientSecret: $intent->client_secret),
        );
        $attempt->setProviderReference($intent->id);

        return $attempt;
    }

    public function captureAttempt(string $providerReference, ?Money $amount = null): CaptureResult
    {
        $this->stripe->paymentIntents->capture($providerReference);

        return new CaptureResult(AttemptStatus::SUCCEEDED, 'succeeded');
    }

    public function parseWebhook(array $payload, array $headers): WebhookResult
    {
        // verify signature, parse event …

        return new WebhookResult(
            providerReference: $payload['data']['object']['id'],
            providerStatus:    $payload['data']['object']['status'],
            newStatus:         AttemptStatus::SUCCEEDED,
            statusChanged:     true,
        );
    }

    // … voidAttempt, initiateRefund, savePaymentMethod
}
```

Register the provider in your `ProviderRegistryInterface` implementation. No changes to this package are needed.

---

Testing
-------

[](#testing)

With Docker (no local PHP required):

```
make test                                    # run all tests
make filter FILTER=testPaymentMarkedSucceeded # run a single test
make shell                                   # open a shell inside the container
```

Without Docker:

```
composer install
vendor/bin/phpunit
vendor/bin/phpunit --filter testPaymentMarkedSucceededOnSyncCapture
```

---

Ecosystem
---------

[](#ecosystem)

PackageDescription`payroad/payroad-core`**This package**`payroad/stripe-provider`Card payments via Stripe`payroad/braintree-provider`Card payments via Braintree`payroad/nowpayments-provider`Crypto payments via NOWPayments`payroad/coingate-provider`Crypto payments via CoinGate`payroad/quickstart`5-minute quickstart — mock card + Stripe`payroad/payroad-symfony-demo`Full reference application (all flows)---

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

20

—

LowBetter than 13% of packages

Maintenance56

Moderate activity, may be stable

Popularity6

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity12

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/ed814123cbd9248f981b882f65d969726dc52dadececdb60670504b22506b1dc?d=identicon)[zykovn](/maintainers/zykovn)

---

Top Contributors

[![zykovnick](https://avatars.githubusercontent.com/u/28895264?v=4)](https://github.com/zykovnick "zykovnick (7 commits)")

### Embed Badge

![Health badge](/badges/payroad-payroad-core/health.svg)

```
[![Health](https://phpackages.com/badges/payroad-payroad-core/health.svg)](https://phpackages.com/packages/payroad-payroad-core)
```

###  Alternatives

[omnipay/coinbase

Coinbase driver for the Omnipay payment processing library

18570.2k1](/packages/omnipay-coinbase)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
