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
2 changes: 1 addition & 1 deletion docs/mbway.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ $order = Order::find(1);

$mbway = new MBWay(
$order->value, // payment value
$order->id, // your identifier (int), echoed back as `identificador` in the callback
$order->id, // your identifier, echoed back as `identificador` in the callback
'912345678', // the customer's MB WAY alias (phone number)
'Order #1' // optional description
);
Expand Down
2 changes: 2 additions & 0 deletions src/CreditCard/CreditCard.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ public function __construct(
*/
public function create(): array
{
$this->clearErrors();

$response = $this->withApiKey()->post($this->getBaseUri().static::URI, $this->getParams());

return $this->mappedReferenceKeys($this->parseTransactionResponse($response));
Expand Down
17 changes: 16 additions & 1 deletion src/EuPago.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public function getBaseUri()
*/
public function create(): array
{
$this->clearErrors();

$response = Http::asForm()->post($this->getBaseUri().static::URI, $this->getParams())->throw();

$referenceData = $response->json();
Expand All @@ -91,6 +93,8 @@ public function create(): array
*/
public function status(string $reference, ?string $entity = null): array
{
$this->clearErrors();

$params = [
'chave' => config('eupago.api_key'),
'referencia' => $reference,
Expand Down Expand Up @@ -129,6 +133,8 @@ public function status(string $reference, ?string $entity = null): array
*/
public function refund(int|string $transactionId, float $amount, ?string $reason = null, ?string $iban = null, ?string $bic = null): array
{
$this->clearErrors();

$params = array_filter([
'amount' => $amount,
'reason' => $reason,
Expand All @@ -137,7 +143,7 @@ public function refund(int|string $transactionId, float $amount, ?string $reason
], fn ($value) => $value !== null);

$response = Http::withToken((new TokenProvider)->token())
->post($this->getBaseUri().static::REFUND_URI.$transactionId, $params);
->post($this->getBaseUri().static::REFUND_URI.rawurlencode((string) $transactionId), $params);

return $this->mappedRefundKeys($this->parseTransactionResponse($response));
}
Expand Down Expand Up @@ -199,6 +205,15 @@ public function hasErrors(): bool
return count($this->errors) > 0;
}

/**
* Empties the error bag, so a reused instance only reports the errors of
* its latest operation.
*/
protected function clearErrors(): void
{
$this->errors = [];
}

/**
* Adds an error to the bag.
*/
Expand Down
4 changes: 2 additions & 2 deletions src/MB/MB.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace CodeTech\EuPago\MB;

use Carbon\Carbon;
use CodeTech\EuPago\EuPago;
use DateTimeInterface;

class MB extends EuPago
{
Expand Down Expand Up @@ -64,7 +64,7 @@ class MB extends EuPago
/**
* MB constructor.
*/
public function __construct(float $value, string $id, Carbon $startDate, Carbon $endDate, float $minValue, float $maxValue, bool $allowDuplication = false)
public function __construct(float $value, string $id, DateTimeInterface $startDate, DateTimeInterface $endDate, float $minValue, float $maxValue, bool $allowDuplication = false)
{
$this->value = $value;
$this->id = $id;
Expand Down
4 changes: 2 additions & 2 deletions src/MBWay/MBWay.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class MBWay extends EuPago
/**
* External identifier. Ex: the order id.
*
* @var int
* @var int|string
*/
protected $id;

Expand All @@ -42,7 +42,7 @@ class MBWay extends EuPago
/**
* MBWay constructor.
*/
public function __construct(float $value, int $id, string $alias, ?string $description = null)
public function __construct(float $value, int|string $id, string $alias, ?string $description = null)
{
$this->value = $value;
$this->id = $id;
Expand Down
6 changes: 5 additions & 1 deletion src/Traits/CreatesEuPagoReferences.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ protected function persistReference(EuPago $payment, string $relation)
return $payment->getErrors();
}

return $this->{$relation}()->create($referenceData);
// The result also carries keys that are not columns (success, response,
// ...), which a strict model refuses after Eupago created the payment.
$reference = $this->{$relation}();

return $reference->create(array_intersect_key($referenceData, array_flip($reference->getRelated()->getFillable())));
}
}
2 changes: 1 addition & 1 deletion src/Traits/HasMbWayReferences.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function mbwayReferences()
* @throws ConnectionException
* @throws RequestException
*/
public function createMbwayReference(float $value, int $id, string $alias, ?string $description = null)
public function createMbwayReference(float $value, int|string $id, string $alias, ?string $description = null)
{
return $this->persistReference(
new MBWay($value, $id, $alias, $description),
Expand Down
4 changes: 2 additions & 2 deletions src/Traits/HasMultibancoReferences.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace CodeTech\EuPago\Traits;

use Carbon\Carbon;
use CodeTech\EuPago\MB\MB;
use CodeTech\EuPago\Models\MbReference;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
Expand All @@ -29,7 +29,7 @@ public function mbReferences()
* @throws ConnectionException
* @throws RequestException
*/
public function createMbReference(float $value, string $id, Carbon $startDate, Carbon $endDate, float $minValue, float $maxValue, bool $allowDuplication = false)
public function createMbReference(float $value, string $id, DateTimeInterface $startDate, DateTimeInterface $endDate, float $minValue, float $maxValue, bool $allowDuplication = false)
{
return $this->persistReference(
new MB($value, $id, $startDate, $endDate, $minValue, $maxValue, $allowDuplication),
Expand Down
24 changes: 24 additions & 0 deletions tests/Feature/RefundTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,27 @@ function fakeRefund(array $refundResponse, int $refundStatus = 201): void

Http::assertSentCount(3);
});

it('encodes the transaction id in the refund URL', function () {
fakeRefund(['transactionStatus' => 'Success', 'refundId' => '12345']);

(new EuPago)->refund('12/34?x', 10.50);

Http::assertSent(fn ($request) => str_ends_with($request->url(), '/refund/12%2F34%3Fx'));
});

it('reports only the errors of the latest refund on a reused instance', function () {
Http::fake([
'*/api/auth/token' => Http::response(['access_token' => 'the-token', 'token_type' => 'Bearer', 'expires_in' => 3600]),
'*/api/management/*' => Http::sequence()
->push(['transactionStatus' => 'Rejected', 'code' => 'AMOUNT', 'text' => 'Invalid amount'], 400)
->push(['transactionStatus' => 'Success', 'refundId' => '12345'], 201),
]);
$eupago = new EuPago;

$eupago->refund(987654, 999.00);
expect($eupago->hasErrors())->toBeTrue();

$eupago->refund(987654, 10.50);
expect($eupago->hasErrors())->toBeFalse();
});
56 changes: 56 additions & 0 deletions tests/Feature/TraitHelpersTest.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Carbon\CarbonImmutable;
use CodeTech\EuPago\Models\CreditCardReference;
use CodeTech\EuPago\Models\MbReference;
use CodeTech\EuPago\Models\MbwayReference;
Expand Down Expand Up @@ -51,6 +52,61 @@ class DummyLegacyPayable extends Model
$this->payable = DummyPayable::create();
});

afterEach(function () {
Model::preventSilentlyDiscardingAttributes(false);
});

it('persists only the reference attributes, so strict models accept them', function () {
Model::preventSilentlyDiscardingAttributes();
Http::fake([
'*/multibanco/*' => Http::response([
'sucesso' => true, 'estado' => 0, 'resposta' => 'OK',
'entidade' => '12345', 'referencia' => '123456789', 'valor' => 10.50,
'valor_minimo' => 10.50, 'valor_maximo' => 10.50,
'data_inicio' => '2026-06-01', 'data_fim' => '2026-06-30',
]),
'*/creditcard/*' => Http::response([
'transactionStatus' => 'Success',
'transactionID' => '6526ds26653sad5489sa32',
'reference' => '00235',
'redirectUrl' => 'https://sandbox.eupago.pt/api/extern/creditcard/form/6526ds26653sad5489sa32',
]),
]);

$mb = $this->payable->createMbReference(10.50, '1', now(), now()->addMonth(), 10.50, 10.50);
$creditCard = $this->payable->createCreditCardReference(
30.00, 'order-50', 'https://shop.test/success', 'https://shop.test/fail', 'https://shop.test/back', 'customer@shop.test'
);

expect($mb)->toBeInstanceOf(MbReference::class)
->and($creditCard)->toBeInstanceOf(CreditCardReference::class);
});

it('creates an MB reference from immutable dates', function () {
Http::fake(['*' => Http::response([
'sucesso' => true, 'estado' => 0, 'resposta' => 'OK',
'entidade' => '12345', 'referencia' => '123456789', 'valor' => 10.50,
'valor_minimo' => 10.50, 'valor_maximo' => 10.50,
'data_inicio' => '2026-06-01', 'data_fim' => '2026-06-30',
])]);

$reference = $this->payable->createMbReference(10.50, '1', CarbonImmutable::parse('2026-06-01'), CarbonImmutable::parse('2026-06-30'), 10.50, 10.50);

expect($reference)->toBeInstanceOf(MbReference::class);
Http::assertSent(fn ($request) => $request['data_inicio'] === '2026-06-01' && $request['data_fim'] === '2026-06-30');
});

it('sends a string MB Way identifier as given', function () {
Http::fake(['*' => Http::response([
'sucesso' => true, 'estado' => 0, 'resposta' => 'OK',
'referencia' => '987654321', 'valor' => 15.00, 'alias' => '912345678',
])]);

$this->payable->createMbwayReference(15.00, '007', '912345678');

Http::assertSent(fn ($request) => $request['id'] === '007');
});

it('creates and persists an MB reference via the trait helper', function () {
Http::fake(['*' => Http::response([
'sucesso' => true, 'estado' => 0, 'resposta' => 'OK',
Expand Down
Loading