From 3c94f898144990ced37c19ed85e51ea4e4fe909b Mon Sep 17 00:00:00 2001 From: jfrosorio Date: Thu, 17 Sep 2026 16:24:01 +0100 Subject: [PATCH 1/2] feat(callbacks): store the Eupago transaction on paid references --- ...add_transaction_id_to_reference_tables.php | 57 +++++++++++++++++++ src/Http/Controllers/MBController.php | 5 +- src/Http/Controllers/MBWayController.php | 5 +- src/Http/Controllers/PayShopController.php | 5 +- .../Controllers/PaysafeCardController.php | 5 +- src/Models/MbReference.php | 1 + src/Models/MbwayReference.php | 1 + src/Models/PayShopReference.php | 1 + src/Models/PaysafeCardReference.php | 1 + tests/Feature/MbCallbackTest.php | 2 + tests/Feature/MbwayCallbackTest.php | 2 + tests/Feature/PayShopCallbackTest.php | 2 + tests/Feature/PaysafeCardCallbackTest.php | 2 + 13 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 database/migrations/2026_09_17_000000_add_transaction_id_to_reference_tables.php diff --git a/database/migrations/2026_09_17_000000_add_transaction_id_to_reference_tables.php b/database/migrations/2026_09_17_000000_add_transaction_id_to_reference_tables.php new file mode 100644 index 0000000..fac0d50 --- /dev/null +++ b/database/migrations/2026_09_17_000000_add_transaction_id_to_reference_tables.php @@ -0,0 +1,57 @@ + + */ + private array $tables = [ + 'mb_references', + 'mbway_references', + 'payshop_references', + 'paysafecard_references', + ]; + + /** + * Run the migrations. + * + * `transaction_id` is the Eupago transaction the callback delivers once + * the payment is made — the id refunds are keyed by. References paid + * before this migration keep a null value: the transaction only ever + * exists in the callback payload, so there is nothing to backfill from. + * + * @return void + */ + public function up() + { + foreach ($this->tables as $table) { + Schema::table($table, function (Blueprint $blueprint) { + $blueprint->string('transaction_id')->nullable()->index()->after('reference'); + }); + } + } + + /** + * Reverse the migrations. + * + * SQLite rebuilds the table on a column drop and validates the surviving + * indexes against it, so the index goes first. + * + * @return void + */ + public function down() + { + foreach ($this->tables as $table) { + Schema::table($table, function (Blueprint $blueprint) { + $blueprint->dropIndex(['transaction_id']); + $blueprint->dropColumn('transaction_id'); + }); + } + } +}; diff --git a/src/Http/Controllers/MBController.php b/src/Http/Controllers/MBController.php index 055318c..ff8f2f8 100644 --- a/src/Http/Controllers/MBController.php +++ b/src/Http/Controllers/MBController.php @@ -28,7 +28,10 @@ public function callback(Request $request) return response()->json(['response' => 'No pending reference found'])->setStatusCode(404); } - $reference->update(['state' => 1]); + $reference->update([ + 'state' => 1, + 'transaction_id' => $validatedData['transacao'], + ]); // trigger event event(new MBReferencePaid($reference)); diff --git a/src/Http/Controllers/MBWayController.php b/src/Http/Controllers/MBWayController.php index 71c6358..094f436 100644 --- a/src/Http/Controllers/MBWayController.php +++ b/src/Http/Controllers/MBWayController.php @@ -28,7 +28,10 @@ public function callback(Request $request) return response()->json(['response' => 'No pending reference found'])->setStatusCode(404); } - $reference->update(['state' => 1]); + $reference->update([ + 'state' => 1, + 'transaction_id' => $validatedData['transacao'], + ]); // trigger event event(new MBWayReferencePaid($reference)); diff --git a/src/Http/Controllers/PayShopController.php b/src/Http/Controllers/PayShopController.php index 44b505f..5e81d44 100644 --- a/src/Http/Controllers/PayShopController.php +++ b/src/Http/Controllers/PayShopController.php @@ -28,7 +28,10 @@ public function callback(Request $request) return response()->json(['response' => 'No pending reference found'])->setStatusCode(404); } - $reference->update(['state' => 1]); + $reference->update([ + 'state' => 1, + 'transaction_id' => $validatedData['transacao'], + ]); // trigger event event(new PayShopReferencePaid($reference)); diff --git a/src/Http/Controllers/PaysafeCardController.php b/src/Http/Controllers/PaysafeCardController.php index bc63efb..94c5e5e 100644 --- a/src/Http/Controllers/PaysafeCardController.php +++ b/src/Http/Controllers/PaysafeCardController.php @@ -28,7 +28,10 @@ public function callback(Request $request) return response()->json(['response' => 'No pending reference found'])->setStatusCode(404); } - $reference->update(['state' => 1]); + $reference->update([ + 'state' => 1, + 'transaction_id' => $validatedData['transacao'], + ]); // trigger event event(new PaysafeCardReferencePaid($reference)); diff --git a/src/Models/MbReference.php b/src/Models/MbReference.php index edf705c..a729ed1 100644 --- a/src/Models/MbReference.php +++ b/src/Models/MbReference.php @@ -12,6 +12,7 @@ class MbReference extends Model protected $fillable = [ 'entity', 'reference', + 'transaction_id', 'value', 'start_date', 'end_date', diff --git a/src/Models/MbwayReference.php b/src/Models/MbwayReference.php index 7c421bc..d1a05d3 100644 --- a/src/Models/MbwayReference.php +++ b/src/Models/MbwayReference.php @@ -11,6 +11,7 @@ class MbwayReference extends Model */ protected $fillable = [ 'reference', + 'transaction_id', 'value', 'alias', 'state', diff --git a/src/Models/PayShopReference.php b/src/Models/PayShopReference.php index dd3b278..41442e8 100644 --- a/src/Models/PayShopReference.php +++ b/src/Models/PayShopReference.php @@ -16,6 +16,7 @@ class PayShopReference extends Model */ protected $fillable = [ 'reference', + 'transaction_id', 'value', 'state', ]; diff --git a/src/Models/PaysafeCardReference.php b/src/Models/PaysafeCardReference.php index 4ee4c9c..d310f7b 100644 --- a/src/Models/PaysafeCardReference.php +++ b/src/Models/PaysafeCardReference.php @@ -17,6 +17,7 @@ class PaysafeCardReference extends Model protected $fillable = [ 'identifier', 'reference', + 'transaction_id', 'url', 'value', 'state', diff --git a/tests/Feature/MbCallbackTest.php b/tests/Feature/MbCallbackTest.php index fae959c..b04fa3f 100644 --- a/tests/Feature/MbCallbackTest.php +++ b/tests/Feature/MbCallbackTest.php @@ -11,6 +11,8 @@ $response->assertOk()->assertJson(['response' => 'Success']); expect((int) $reference->fresh()->state)->toBe(1); + // The callback's `transacao` is the id refunds are keyed by. + expect($reference->fresh()->transaction_id)->toBe('TXN123'); Event::assertDispatched( MBReferencePaid::class, fn (MBReferencePaid $event) => $event->reference->is($reference) diff --git a/tests/Feature/MbwayCallbackTest.php b/tests/Feature/MbwayCallbackTest.php index b5042e8..f3255ff 100644 --- a/tests/Feature/MbwayCallbackTest.php +++ b/tests/Feature/MbwayCallbackTest.php @@ -11,6 +11,8 @@ $response->assertOk()->assertJson(['response' => 'Success']); expect((int) $reference->fresh()->state)->toBe(1); + // The callback's `transacao` is the id refunds are keyed by. + expect($reference->fresh()->transaction_id)->toBe('TXN999'); Event::assertDispatched( MBWayReferencePaid::class, fn (MBWayReferencePaid $event) => $event->reference->is($reference) diff --git a/tests/Feature/PayShopCallbackTest.php b/tests/Feature/PayShopCallbackTest.php index 6d27a9f..e143733 100644 --- a/tests/Feature/PayShopCallbackTest.php +++ b/tests/Feature/PayShopCallbackTest.php @@ -11,6 +11,8 @@ $response->assertOk()->assertJson(['response' => 'Success']); expect((int) $reference->fresh()->state)->toBe(1); + // The callback's `transacao` is the id refunds are keyed by. + expect($reference->fresh()->transaction_id)->toBe('TXN555'); Event::assertDispatched( PayShopReferencePaid::class, fn (PayShopReferencePaid $event) => $event->reference->is($reference) diff --git a/tests/Feature/PaysafeCardCallbackTest.php b/tests/Feature/PaysafeCardCallbackTest.php index c81db9a..dfbbf5f 100644 --- a/tests/Feature/PaysafeCardCallbackTest.php +++ b/tests/Feature/PaysafeCardCallbackTest.php @@ -11,6 +11,8 @@ $response->assertOk()->assertJson(['response' => 'Success']); expect((int) $reference->fresh()->state)->toBe(1); + // The callback's `transacao` is the id refunds are keyed by. + expect($reference->fresh()->transaction_id)->toBe('29749250'); Event::assertDispatched( PaysafeCardReferencePaid::class, fn (PaysafeCardReferencePaid $event) => $event->reference->is($reference) From 9e8685965c0614664facc17eecabff01ad08af2d Mon Sep 17 00:00:00 2001 From: jfrosorio Date: Thu, 17 Sep 2026 16:24:01 +0100 Subject: [PATCH 2/2] docs(refunds): document the transaction stored on paid references --- UPGRADE.md | 16 ++++++++++++++++ docs/mbway.md | 4 +++- docs/multibanco.md | 4 +++- docs/paysafecard.md | 4 +++- docs/payshop.md | 4 +++- docs/refunds.md | 3 ++- 6 files changed, 30 insertions(+), 5 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 20c7fce..6027eb4 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,21 @@ # Upgrading +## 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: + +```bash +php artisan vendor:publish --provider=CodeTech\\EuPago\\Providers\\EuPagoServiceProvider --tag=migrations +``` + +Run the new migration: + +```bash +php artisan migrate +``` + +References paid before the upgrade keep a null `transaction_id` — the value only ever exists in the callback payload, so there is nothing to backfill from. + ## From v3.7.x to v3.8.0 This release adds Credit Card support, which uses a new `credit_card_references` table. Re-publish the migrations (existing files are left untouched) and run the new one: diff --git a/docs/mbway.md b/docs/mbway.md index 1f4dd1b..a77b512 100644 --- a/docs/mbway.md +++ b/docs/mbway.md @@ -55,4 +55,6 @@ Retrieve the MB WAY references: $mbwayReferences = $order->mbwayReferences; ``` -When the payment is confirmed, the [callback](callbacks.md) fires an `MBWayReferencePaid` event. +When the payment is confirmed, the [callback](callbacks.md) fires an `MBWayReferencePaid` +event and stores the Eupago transaction on the reference, so a paid reference can be +[refunded](refunds.md) through `$reference->transaction_id`. diff --git a/docs/multibanco.md b/docs/multibanco.md index 3577c89..e728af3 100644 --- a/docs/multibanco.md +++ b/docs/multibanco.md @@ -74,4 +74,6 @@ Retrieve the MB references: $mbReferences = $order->mbReferences; ``` -When the reference is paid, the [callback](callbacks.md) fires an `MBReferencePaid` event. +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`. diff --git a/docs/paysafecard.md b/docs/paysafecard.md index d56e8d3..8dae30c 100644 --- a/docs/paysafecard.md +++ b/docs/paysafecard.md @@ -79,4 +79,6 @@ Retrieve the PaysafeCard references: $paysafeCardReferences = $order->paysafeCardReferences; ``` -When the payment is completed, the [callback](callbacks.md) fires a `PaysafeCardReferencePaid` event. +When the payment is completed, the [callback](callbacks.md) fires a `PaysafeCardReferencePaid` +event and stores the Eupago transaction on the reference, so a paid reference can be +[refunded](refunds.md) through `$reference->transaction_id`. diff --git a/docs/payshop.md b/docs/payshop.md index b323768..a3d229d 100644 --- a/docs/payshop.md +++ b/docs/payshop.md @@ -64,4 +64,6 @@ Retrieve the PayShop references: $payShopReferences = $order->payShopReferences; ``` -When the reference is paid, the [callback](callbacks.md) fires a `PayShopReferencePaid` event. +When the reference is paid, the [callback](callbacks.md) fires a `PayShopReferencePaid` +event and stores the Eupago transaction on the reference, so a paid reference can be +[refunded](refunds.md) through `$reference->transaction_id`. diff --git a/docs/refunds.md b/docs/refunds.md index 1c68b7f..9640499 100644 --- a/docs/refunds.md +++ b/docs/refunds.md @@ -7,7 +7,8 @@ group: Handling payments Paid transactions can be refunded, partially or in full, through Eupago's management API. Refunds require the [OAuth client credentials](configuration.md#oauth-client-credentials) to be configured. The refund is keyed by the transaction id — the `transacao` value -delivered by the payment [callback](callbacks.md): +delivered by the payment [callback](callbacks.md), which the package also stores on the +reference it marks as paid, so a stored reference carries it as `transaction_id`: ```php use CodeTech\EuPago\EuPago;