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
36 changes: 36 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,42 @@ If `resources/lang` is now empty, remove it so Laravel goes back to `lang/`:
rmdir resources/lang/vendor resources/lang
```

Eupago sends every payment method's notification to the single URL a channel takes, so the per-method callback endpoints used to confirm only the payments of the method whose URL was set. There is now a single endpoint, and the per-method ones are aliases of it, so every method is confirmed whichever URL is set. The per-method endpoints are deprecated and will be removed in v4, so in the Eupago backoffice, set the channel's notification URL to the new endpoint:

```
https://your-app.test/eupago/callback
```

The per-method routes now point at `CallbackController`, so build their URLs by route name (`route('eupago.mb.callback')`) rather than by controller action.

If you disabled the package routes and mounted a per-method controller on a route of your own, it still confirms only that method. Point your route at `CallbackController` instead. Keep its path, so the URL set in the backoffice keeps working, and set its method as the fallback for a notification whose `mp` the package does not know. Take it out of the `web` middleware group too, which stores the URL — API key included — in the session:

```php
use CodeTech\EuPago\Enums\PaymentMethod;
use CodeTech\EuPago\Http\Controllers\CallbackController;

Route::get('webhooks/eupago/mb', [CallbackController::class, 'callback'])
->defaults('default_payment_method', PaymentMethod::Multibanco->value)
->withoutMiddleware('web')
->name('eupago.mb.callback');
```

The paid events now fire once the payment is stored, so queued listeners always find it. A notification Eupago delivers again now gets a 200 instead of a 404, still without firing the event.

Multibanco payments are now recorded in a table of their own, so references with an amount range or with repeat payments can be confirmed. Re-publish the migrations, which leaves the existing files untouched:

```bash
php artisan vendor:publish --provider=CodeTech\\EuPago\\Providers\\EuPagoServiceProvider --tag=eupago-migrations
```

Run the new migration, which also records the payment of every reference paid since v3.9.0, when the transaction started being stored:

```bash
php artisan migrate
```

A Multibanco reference with an amount range is now confirmed for any amount within it, so the payment can be less than the reference's value. If your references accept a range, compare `$event->payment?->value` in your `MBReferencePaid` listeners with the amount owed.

## From v3.8.x to v3.9.0

Multibanco, MB WAY, PayShop and PaysafeCard references now store the Eupago transaction their callback delivers, so a paid reference can be refunded through `$reference->transaction_id`. A new migration adds the column. Re-publish the migrations, which leaves the existing files untouched:
Expand Down
4 changes: 2 additions & 2 deletions config/eupago.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@
| Routes
|--------------------------------------------------------------------------
|
| The package automatically registers the payment callback routes
| (e.g. /eupago/mb/callback). Disable this if you use the package as
| The package automatically registers the payment callback route
| (/eupago/callback). Disable this if you use the package as
| a thin API client and handle EuPago's webhooks yourself.
|
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*
* An MB reference that allows repeat payments, or accepts an amount
* range, can be paid more than once and for amounts other than its
* value — each payment gets its own row, keyed by the Eupago transaction
* so a repeated delivery is only recorded once.
*
* References already paid get their payment backfilled, so a repeated
* delivery of it is recognised too. Before this table, only the exact
* value could be confirmed, so it is the amount that was paid.
*
* @return void
*/
public function up()
{
Schema::create('mb_reference_payments', function (Blueprint $table) {
$table->id();
$table->foreignId('mb_reference_id')->index()->constrained()->cascadeOnDelete();
$table->string('transaction_id')->unique();
$table->decimal('value', 10, 2);
$table->timestamps();
});

DB::table('mb_reference_payments')->insertUsing(
['mb_reference_id', 'transaction_id', 'value', 'created_at', 'updated_at'],
DB::table('mb_references')
->select(['id', 'transaction_id', 'value', 'updated_at', 'updated_at'])
->where('state', 1)
->whereNotNull('transaction_id')
);
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('mb_reference_payments');
}
};
60 changes: 45 additions & 15 deletions docs/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,53 @@ weight: 10
group: Handling payments
---

Eupago notifies your application of confirmed payments through a webhook — configure the
callback URL in the [Eupago backoffice](https://clientes.eupago.pt) for your channel.
The package registers one endpoint per payment method:
Eupago notifies your application of confirmed payments through a webhook. The
[Eupago backoffice](https://clientes.eupago.pt) takes a single notification URL per channel and
sends every payment method's notification to it, so set it to the package's callback endpoint:

| Payment method | Callback endpoint | Event fired |
|----------------|-------------------------------|--------------------------|
| Multibanco | `GET /eupago/mb/callback` | `MBReferencePaid` |
| MB WAY | `GET /eupago/mbway/callback` | `MBWayReferencePaid` |
| PayShop | `GET /eupago/payshop/callback`| `PayShopReferencePaid` |
| PaysafeCard | `GET /eupago/paysafecard/callback` | `PaysafeCardReferencePaid` |
| Credit Card | `GET /eupago/creditcard/callback` | `CreditCardReferencePaid` |
```
https://your-app.test/eupago/callback
```

Each callback validates the payload (including the channel and API key), matches the
pending reference against the values Eupago echoes back, marks it as paid, and fires the
corresponding event with the reference as payload.
The endpoint reads the payment method from the notification (`mp`) and handles it:

All callbacks receive the same query parameters:
| Payment method | `mp` | Event fired |
|----------------|---------|----------------------------|
| Multibanco | `PC:PT` | `MBReferencePaid` |
| MB WAY | `MW:PT` | `MBWayReferencePaid` |
| PayShop | `PS:PT` | `PayShopReferencePaid` |
| PaysafeCard | `PF:PT` | `PaysafeCardReferencePaid` |
| Credit Card | `CC:PT` | `CreditCardReferencePaid` |

The callback first checks the channel and API key, then matches the reference against the
values Eupago echoes back, marks it as paid and fires the method's event with the reference as
payload. The event fires once the payment is stored, and only once per payment: a notification
Eupago delivers again is acknowledged without firing it. That also holds when a listener throws,
so put work that can fail, such as calling another service, in a
[queued listener](https://laravel.com/docs/events#queued-event-listeners), which the queue retries.

Multibanco references can accept an amount range and allow repeat payments, so each payment is
recorded on its own, and the `MBReferencePaid` event carries it next to the reference. With an
amount range, the payment can be less than the reference's value, so compare the amount paid
with what is owed:

```php
public function handle(MBReferencePaid $event): void
{
$event->payment?->value; // the amount paid
$event->payment?->transaction_id; // the Eupago transaction
}
```

The payment is null when you dispatch the event yourself without one.

The per-method endpoints of earlier versions — `/eupago/mb/callback`, `/eupago/mbway/callback`,
`/eupago/payshop/callback`, `/eupago/paysafecard/callback` and `/eupago/creditcard/callback` — are
deprecated and will be removed in v4. They are now aliases of `/eupago/callback`, so a URL already
set in the backoffice keeps working for every payment method, but switch it to
`/eupago/callback` before upgrading to v4.

The callback receives these query parameters:

| Name | Type | Required |
|---------------|-------------------------------|:--------:|
Expand All @@ -36,5 +66,5 @@ All callbacks receive the same query parameters:
| comissao | float | yes |
| local | string | no |

To mount the callback controllers on routes of your own instead of the automatically
To mount the callback controller on a route of your own instead of the automatically
registered ones, see [Routes](configuration.md#routes).
15 changes: 9 additions & 6 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ status, you can leave these empty.
The package supports two levels of usage:

- **Full integration** (default): use the traits and models to persist references, and let the
package handle Eupago's webhooks — it registers the callback routes (`/eupago/*/callback`)
package handle Eupago's webhooks — it registers the [callback](callbacks.md) endpoint (`/eupago/callback`)
automatically.
- **Thin API client**: use only the payment classes (e.g. `(new MB(...))->create()`) and handle
persistence and webhooks yourself.
Expand All @@ -52,16 +52,19 @@ If you only need the thin client, disable the automatic route registration:
EUPAGO_ROUTES=false
```

With the routes disabled you can still mount the package's callback controllers on routes of
With the routes disabled you can still mount the package's callback controller on a route of
your own, giving you full control over the path and middleware:

```php
use CodeTech\EuPago\Http\Controllers\MBController;
use CodeTech\EuPago\Http\Controllers\CallbackController;

Route::get('webhooks/eupago/mb', [MBController::class, 'callback'])
->middleware('web')
->name('eupago.mb.callback');
Route::get('webhooks/eupago', [CallbackController::class, 'callback'])
->withoutMiddleware('web')
->name('eupago.callback');
```

Keep the route out of the `web` middleware group: a webhook has no use for a session, and the
group would start one on every call, storing its URL — API key included.

> **Note:** if your application caches routes, run `php artisan route:clear` after changing
> this setting.
8 changes: 8 additions & 0 deletions docs/multibanco.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,11 @@ $mbReferences = $order->mbReferences;
When the reference is paid, the [callback](callbacks.md) fires an `MBReferencePaid` event
and stores the Eupago transaction on the reference, so a paid reference can be
[refunded](refunds.md) through `$reference->transaction_id`.

A reference with a minimum and maximum value accepts any amount in that range, and one that
allows duplicated payments can be paid more than once. Each payment is recorded with the amount
paid and its transaction, and the reference keeps the latest one:

```php
$payments = $reference->payments;
```
47 changes: 21 additions & 26 deletions routes/web.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
<?php

use CodeTech\EuPago\Http\Controllers\CreditCardController;
use CodeTech\EuPago\Http\Controllers\MBController;
use CodeTech\EuPago\Http\Controllers\MBWayController;
use CodeTech\EuPago\Http\Controllers\PaysafeCardController;
use CodeTech\EuPago\Http\Controllers\PayShopController;
use CodeTech\EuPago\Enums\PaymentMethod;
use CodeTech\EuPago\Http\Controllers\CallbackController;
use Illuminate\Support\Facades\Route;

/*
Expand All @@ -13,27 +10,25 @@
|--------------------------------------------------------------------------
*/

// MB
Route::prefix('mb')->name('mb.')->group(function () {
Route::get('callback', [MBController::class, 'callback'])->name('callback');
});
Route::get('callback', [CallbackController::class, 'callback'])->name('callback');

// MB Way
Route::prefix('mbway')->name('mbway.')->group(function () {
Route::get('callback', [MBWayController::class, 'callback'])->name('callback');
});

// PayShop
Route::prefix('payshop')->name('payshop.')->group(function () {
Route::get('callback', [PayShopController::class, 'callback'])->name('callback');
});
/*
| Deprecated, to be removed in v4. A channel takes a single notification URL,
| so these are aliases of the endpoint above, kept for the URLs already set
| in Eupago's backoffice. Each falls back to its own payment method when a
| notification's `mp` is not one the package knows.
*/

// PaysafeCard
Route::prefix('paysafecard')->name('paysafecard.')->group(function () {
Route::get('callback', [PaysafeCardController::class, 'callback'])->name('callback');
});
$aliases = [
'mb' => PaymentMethod::Multibanco,
'mbway' => PaymentMethod::MbWay,
'payshop' => PaymentMethod::PayShop,
'paysafecard' => PaymentMethod::PaysafeCard,
'creditcard' => PaymentMethod::CreditCard,
];

// Credit Card
Route::prefix('creditcard')->name('creditcard.')->group(function () {
Route::get('callback', [CreditCardController::class, 'callback'])->name('callback');
});
foreach ($aliases as $prefix => $method) {
Route::get("{$prefix}/callback", [CallbackController::class, 'callback'])
->defaults('default_payment_method', $method->value)
->name("{$prefix}.callback");
}
16 changes: 16 additions & 0 deletions src/Enums/PaymentMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace CodeTech\EuPago\Enums;

/**
* The payment methods the package handles, backed by the code Eupago's
* notifications identify them with (`mp`).
*/
enum PaymentMethod: string
{
case Multibanco = 'PC:PT';
case MbWay = 'MW:PT';
case PayShop = 'PS:PT';
case PaysafeCard = 'PF:PT';
case CreditCard = 'CC:PT';
}
13 changes: 12 additions & 1 deletion src/Events/MBReferencePaid.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace CodeTech\EuPago\Events;

use CodeTech\EuPago\Models\MbReference;
use CodeTech\EuPago\Models\MbReferencePayment;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
Expand All @@ -19,12 +20,22 @@ class MBReferencePaid
*/
public $reference;

/**
* The payment that was made — a reference that allows repeat payments,
* or accepts an amount range, can be paid more than once and for
* amounts other than its value.
*
* @var MbReferencePayment|null
*/
public $payment;

/**
* MBReferencePaid constructor.
*/
public function __construct(MbReference $reference)
public function __construct(MbReference $reference, ?MbReferencePayment $payment = null)
{
$this->reference = $reference;
$this->payment = $payment;
}

/**
Expand Down
Loading
Loading