diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 7224182..1204bfc 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -9,6 +9,8 @@
+
+
diff --git a/src/Payment/DTO/PaymentDTO.php b/src/Payment/DTO/PaymentDTO.php
index db0510d..a82d270 100644
--- a/src/Payment/DTO/PaymentDTO.php
+++ b/src/Payment/DTO/PaymentDTO.php
@@ -5,7 +5,7 @@
use Saloon\Http\Response;
use Saloon\Traits\Responses\HasResponse;
-final readonly class PaymentDTO
+final class PaymentDTO
{
use HasResponse;
diff --git a/tests/Config/MockResponses.php b/tests/Config/MockResponses.php
index 5de949d..2357dbd 100644
--- a/tests/Config/MockResponses.php
+++ b/tests/Config/MockResponses.php
@@ -4,12 +4,20 @@
use Saloon\Http\Faking\MockResponse;
use Tests\Config\Samples\InvoiceSamples;
+use Tests\Config\Samples\PaymentSamples;
use HamodaDev\Moyasar\Invoice\APIs\CancelInvoiceRequest;
use HamodaDev\Moyasar\Invoice\APIs\BulkCreateInvoicesRequest;
use HamodaDev\Moyasar\Invoice\APIs\CreateInvoiceRequest;
use HamodaDev\Moyasar\Invoice\APIs\GetInvoiceRequest;
use HamodaDev\Moyasar\Invoice\APIs\ListInvoicesRequest;
use HamodaDev\Moyasar\Invoice\APIs\UpdateInvoiceRequest;
+use HamodaDev\Moyasar\Payment\APIs\CapturePaymentRequest;
+use HamodaDev\Moyasar\Payment\APIs\CreatePaymentRequest;
+use HamodaDev\Moyasar\Payment\APIs\GetPaymentRequest;
+use HamodaDev\Moyasar\Payment\APIs\ListPaymentsRequest;
+use HamodaDev\Moyasar\Payment\APIs\RefundPaymentRequest;
+use HamodaDev\Moyasar\Payment\APIs\UpdatePaymentRequest;
+use HamodaDev\Moyasar\Payment\APIs\VoidPaymentRequest;
class MockResponses
{
@@ -17,7 +25,7 @@ public static function getAll(): array
{
return array_merge(
self::invoiceResponses(),
- // self::paymentResponses(), // TODO: add when payment mocks are ready
+ self::paymentResponses(),
);
}
@@ -43,4 +51,23 @@ private static function invoiceResponses(): array
CancelInvoiceRequest::class => MockResponse::make(body: InvoiceSamples::CANCELED_TEST_INVOICE),
];
}
+
+ private static function paymentResponses(): array
+ {
+ return [
+ CreatePaymentRequest::class => MockResponse::make(body: PaymentSamples::TEST_PAYMENT),
+ GetPaymentRequest::class => MockResponse::make(body: PaymentSamples::TEST_PAYMENT_3),
+ ListPaymentsRequest::class => MockResponse::make(body: [
+ 'payments' => [
+ PaymentSamples::TEST_PAYMENT,
+ PaymentSamples::TEST_PAYMENT_2,
+ PaymentSamples::TEST_PAYMENT_3,
+ ],
+ ]),
+ UpdatePaymentRequest::class => MockResponse::make(body: PaymentSamples::TEST_PAYMENT),
+ RefundPaymentRequest::class => MockResponse::make(body: PaymentSamples::TEST_REFUNDED_PAYMENT),
+ CapturePaymentRequest::class => MockResponse::make(body: PaymentSamples::TEST_CAPTURED_PAYMENT),
+ VoidPaymentRequest::class => MockResponse::make(body: PaymentSamples::TEST_VOIDED_PAYMENT),
+ ];
+ }
}
diff --git a/tests/Config/Samples/PaymentSamples.php b/tests/Config/Samples/PaymentSamples.php
new file mode 100644
index 0000000..d4c522b
--- /dev/null
+++ b/tests/Config/Samples/PaymentSamples.php
@@ -0,0 +1,208 @@
+ 'pay_1',
+ 'status' => 'paid',
+ 'amount' => 10000,
+ 'fee' => 50,
+ 'currency' => 'SAR',
+ 'refunded' => 0,
+ 'refunded_at' => null,
+ 'captured' => 10000,
+ 'captured_at' => '2024-01-15T12:00:00Z',
+ 'voided_at' => null,
+ 'description' => 'Test payment',
+ 'amount_format' => '100.00 SAR',
+ 'fee_format' => '0.50 SAR',
+ 'refunded_format' => '0.00 SAR',
+ 'captured_format' => '100.00 SAR',
+ 'invoice_id' => null,
+ 'ip' => '127.0.0.1',
+ 'callback_url' => 'https://example.com/webhooks/moyasar',
+ 'created_at' => '2024-01-15T10:00:00Z',
+ 'updated_at' => '2024-01-15T12:00:00Z',
+ 'metadata' => [
+ 'order_id' => '1234',
+ ],
+ 'source' => [
+ 'type' => 'creditcard',
+ 'company' => 'visa',
+ 'name' => 'John Doe',
+ 'number' => '**** **** **** 1111',
+ 'message' => null,
+ ],
+ 'given_id' => null,
+ ];
+
+ public const array TEST_PAYMENT_2 = [
+ 'id' => 'pay_2',
+ 'status' => 'paid',
+ 'amount' => 2500,
+ 'fee' => 25,
+ 'currency' => 'SAR',
+ 'refunded' => 0,
+ 'refunded_at' => null,
+ 'captured' => 2500,
+ 'captured_at' => '2024-02-01T08:00:00Z',
+ 'voided_at' => null,
+ 'description' => 'Second test payment',
+ 'amount_format' => '25.00 SAR',
+ 'fee_format' => '0.25 SAR',
+ 'refunded_format' => '0.00 SAR',
+ 'captured_format' => '25.00 SAR',
+ 'invoice_id' => null,
+ 'ip' => '192.168.1.1',
+ 'callback_url' => null,
+ 'created_at' => '2024-02-01T07:00:00Z',
+ 'updated_at' => '2024-02-01T08:00:00Z',
+ 'metadata' => [],
+ 'source' => [
+ 'type' => 'creditcard',
+ 'company' => 'mastercard',
+ 'name' => 'Jane Doe',
+ 'number' => '**** **** **** 4242',
+ 'message' => null,
+ ],
+ 'given_id' => 'order_5678',
+ ];
+
+ public const array TEST_PAYMENT_3 = [
+ 'id' => 'pay_3',
+ 'status' => 'paid',
+ 'amount' => 6900,
+ 'fee' => 35,
+ 'currency' => 'EGP',
+ 'refunded' => 0,
+ 'refunded_at' => null,
+ 'captured' => 6900,
+ 'captured_at' => '2024-03-01T09:00:00Z',
+ 'voided_at' => null,
+ 'description' => 'Third test payment',
+ 'amount_format' => '69.00 EGP',
+ 'fee_format' => '0.35 EGP',
+ 'refunded_format' => '0.00 EGP',
+ 'captured_format' => '69.00 EGP',
+ 'invoice_id' => null,
+ 'ip' => '10.0.0.1',
+ 'callback_url' => null,
+ 'created_at' => '2024-03-01T08:00:00Z',
+ 'updated_at' => '2024-03-01T09:00:00Z',
+ 'metadata' => [
+ 'order_id' => '333',
+ ],
+ 'source' => [
+ 'type' => 'creditcard',
+ 'company' => 'visa',
+ 'name' => 'Test User',
+ 'number' => '**** **** **** 8888',
+ 'message' => null,
+ ],
+ 'given_id' => null,
+ ];
+
+ public const array TEST_REFUNDED_PAYMENT = [
+ 'id' => 'pay_1',
+ 'status' => 'refunded',
+ 'amount' => 10000,
+ 'fee' => 50,
+ 'currency' => 'SAR',
+ 'refunded' => 10000,
+ 'refunded_at' => '2024-01-20T14:00:00Z',
+ 'captured' => 10000,
+ 'captured_at' => '2024-01-15T12:00:00Z',
+ 'voided_at' => null,
+ 'description' => 'Test payment',
+ 'amount_format' => '100.00 SAR',
+ 'fee_format' => '0.50 SAR',
+ 'refunded_format' => '100.00 SAR',
+ 'captured_format' => '100.00 SAR',
+ 'invoice_id' => null,
+ 'ip' => '127.0.0.1',
+ 'callback_url' => 'https://example.com/webhooks/moyasar',
+ 'created_at' => '2024-01-15T10:00:00Z',
+ 'updated_at' => '2024-01-20T14:00:00Z',
+ 'metadata' => [
+ 'order_id' => '1234',
+ ],
+ 'source' => [
+ 'type' => 'creditcard',
+ 'company' => 'visa',
+ 'name' => 'John Doe',
+ 'number' => '**** **** **** 1111',
+ 'message' => null,
+ ],
+ 'given_id' => null,
+ ];
+
+ public const array TEST_CAPTURED_PAYMENT = [
+ 'id' => 'pay_1',
+ 'status' => 'captured',
+ 'amount' => 10000,
+ 'fee' => 50,
+ 'currency' => 'SAR',
+ 'refunded' => 0,
+ 'refunded_at' => null,
+ 'captured' => 10000,
+ 'captured_at' => '2024-01-15T12:00:00Z',
+ 'voided_at' => null,
+ 'description' => 'Test payment',
+ 'amount_format' => '100.00 SAR',
+ 'fee_format' => '0.50 SAR',
+ 'refunded_format' => '0.00 SAR',
+ 'captured_format' => '100.00 SAR',
+ 'invoice_id' => null,
+ 'ip' => '127.0.0.1',
+ 'callback_url' => 'https://example.com/webhooks/moyasar',
+ 'created_at' => '2024-01-15T10:00:00Z',
+ 'updated_at' => '2024-01-15T12:00:00Z',
+ 'metadata' => [
+ 'order_id' => '1234',
+ ],
+ 'source' => [
+ 'type' => 'creditcard',
+ 'company' => 'visa',
+ 'name' => 'John Doe',
+ 'number' => '**** **** **** 1111',
+ 'message' => null,
+ ],
+ 'given_id' => null,
+ ];
+
+ public const array TEST_VOIDED_PAYMENT = [
+ 'id' => 'pay_1',
+ 'status' => 'voided',
+ 'amount' => 10000,
+ 'fee' => 50,
+ 'currency' => 'SAR',
+ 'refunded' => 0,
+ 'refunded_at' => null,
+ 'captured' => 0,
+ 'captured_at' => null,
+ 'voided_at' => '2024-01-16T10:00:00Z',
+ 'description' => 'Test payment',
+ 'amount_format' => '100.00 SAR',
+ 'fee_format' => '0.50 SAR',
+ 'refunded_format' => '0.00 SAR',
+ 'captured_format' => '0.00 SAR',
+ 'invoice_id' => null,
+ 'ip' => '127.0.0.1',
+ 'callback_url' => 'https://example.com/webhooks/moyasar',
+ 'created_at' => '2024-01-15T10:00:00Z',
+ 'updated_at' => '2024-01-16T10:00:00Z',
+ 'metadata' => [
+ 'order_id' => '1234',
+ ],
+ 'source' => [
+ 'type' => 'creditcard',
+ 'company' => 'visa',
+ 'name' => 'John Doe',
+ 'number' => '**** **** **** 1111',
+ 'message' => null,
+ ],
+ 'given_id' => null,
+ ];
+}
diff --git a/tests/Feature/Payment/CapturePaymentTest.php b/tests/Feature/Payment/CapturePaymentTest.php
new file mode 100644
index 0000000..b608705
--- /dev/null
+++ b/tests/Feature/Payment/CapturePaymentTest.php
@@ -0,0 +1,29 @@
+ validateEnvIsSet());
+
+it('captures an authorized payment', function () {
+ // arrange
+ $moyasar = MoyasarInitializer::getInstance()->getMoyasar();
+
+ $payment = $moyasar->payment()->create(CreatePaymentDTO::fromArray(mockCreatePaymentDTO()));
+
+ $paymentId = getenv('MOYASAR_SAMPLE_AUTHORIZED_PAYMENT_ID') ?: $payment->id;
+
+ // act
+ $captured = $moyasar->payment()->capture($paymentId);
+
+ // assert
+ expect($captured)->toBeInstanceOf(PaymentDTO::class)
+ ->and($captured->id)->toBe($paymentId)
+ ->and($captured->status)->toBe('captured')
+ ->and($captured->captured)->toBeGreaterThan(0)
+ ->and($captured->capturedAt)->not->toBeNull();
+})->skip(
+ fn () => getenv('REAL_TESTS') === 'true' && empty(getenv('MOYASAR_SAMPLE_AUTHORIZED_PAYMENT_ID')),
+ 'MOYASAR_SAMPLE_AUTHORIZED_PAYMENT_ID is required for real capture tests'
+);
diff --git a/tests/Feature/Payment/CreatePaymentTest.php b/tests/Feature/Payment/CreatePaymentTest.php
new file mode 100644
index 0000000..2d9e806
--- /dev/null
+++ b/tests/Feature/Payment/CreatePaymentTest.php
@@ -0,0 +1,54 @@
+ validateEnvIsSet());
+
+it('creates a payment with credit card source', function () {
+ // arrange
+ $moyasar = MoyasarInitializer::getInstance()->getMoyasar();
+
+ // act
+ $payment = $moyasar->payment()->create(new CreatePaymentDTO(
+ amount: 10000,
+ currency: 'SAR',
+ description: 'Test payment',
+ callbackUrl: 'https://example.com/webhook',
+ source: new CreditCardSourceDTO(
+ name: 'John Doe',
+ number: '4111111111111111',
+ month: 12,
+ year: 2030,
+ cvc: 123,
+ ),
+ ));
+
+ // assert
+ expect($payment)->toBeInstanceOf(PaymentDTO::class)
+ ->and($payment->id)->not->toBeEmpty()
+ ->and($payment->status)->toBeIn(['initiated', 'paid'])
+ ->and($payment->amount)->toBe(10000)
+ ->and($payment->currency)->toBe('SAR')
+ ->and($payment->source)->toBeArray()
+ ->and($payment->source['type'])->toBe('creditcard');
+});
+
+it('creates a payment using fromArray with array source', function () {
+ // arrange
+ $moyasar = MoyasarInitializer::getInstance()->getMoyasar();
+
+ // act
+ $payment = $moyasar->payment()->create(CreatePaymentDTO::fromArray(mockCreatePaymentDTO([
+ 'amount' => 6900,
+ 'currency' => 'EGP',
+ ])));
+
+ // assert
+ expect($payment)->toBeInstanceOf(PaymentDTO::class)
+ ->and($payment->id)->not->toBeEmpty()
+ ->and($payment->amount)->toBeIn([10000, 6900])
+ ->and($payment->source)->toBeArray();
+});
diff --git a/tests/Feature/Payment/GetPaymentTest.php b/tests/Feature/Payment/GetPaymentTest.php
new file mode 100644
index 0000000..ed53bf1
--- /dev/null
+++ b/tests/Feature/Payment/GetPaymentTest.php
@@ -0,0 +1,35 @@
+ validateEnvIsSet());
+
+it('retrieves payment data', function () {
+ // arrange
+ $moyasar = MoyasarInitializer::getInstance()->getMoyasar();
+
+ $payment = $moyasar->payment()->create(CreatePaymentDTO::fromArray(mockCreatePaymentDTO()));
+ $paymentId = $payment->id;
+
+ // act
+ $retrieved = $moyasar->payment()->get($paymentId);
+
+ // assert
+ expect($retrieved)
+ ->toBeInstanceOf(PaymentDTO::class)
+ ->and($retrieved->id)->not->toBeEmpty();
+});
+
+it('lists payments', function () {
+ // arrange
+ $moyasar = MoyasarInitializer::getInstance()->getMoyasar();
+
+ // act
+ $payments = iterator_to_array($moyasar->payment()->list()->paginate($moyasar)->items());
+
+ // assert
+ expect($payments)->not->toBeEmpty()
+ ->and($payments[0])->toBeInstanceOf(PaymentDTO::class);
+});
diff --git a/tests/Feature/Payment/RefundPaymentTest.php b/tests/Feature/Payment/RefundPaymentTest.php
new file mode 100644
index 0000000..847d601
--- /dev/null
+++ b/tests/Feature/Payment/RefundPaymentTest.php
@@ -0,0 +1,49 @@
+ validateEnvIsSet());
+
+it('refunds a full payment', function () {
+ // arrange
+ $moyasar = MoyasarInitializer::getInstance()->getMoyasar();
+
+ $payment = $moyasar->payment()->create(CreatePaymentDTO::fromArray(mockCreatePaymentDTO()));
+
+ $paymentId = getenv('MOYASAR_SAMPLE_PAID_PAYMENT_ID') ?: $payment->id;
+
+ // act
+ $refunded = $moyasar->payment()->refund($paymentId);
+
+ // assert
+ expect($refunded)->toBeInstanceOf(PaymentDTO::class)
+ ->and($refunded->id)->toBe($paymentId)
+ ->and($refunded->status)->toBe('refunded')
+ ->and($refunded->refunded)->toBeGreaterThan(0)
+ ->and($refunded->refundedAt)->not->toBeNull();
+})->skip(
+ fn () => getenv('REAL_TESTS') === 'true' && empty(getenv('MOYASAR_SAMPLE_PAID_PAYMENT_ID')),
+ 'MOYASAR_SAMPLE_PAID_PAYMENT_ID is required for real refund tests'
+);
+
+it('refunds a partial payment', function () {
+ // arrange
+ $moyasar = MoyasarInitializer::getInstance()->getMoyasar();
+
+ $payment = $moyasar->payment()->create(CreatePaymentDTO::fromArray(mockCreatePaymentDTO()));
+
+ $paymentId = getenv('MOYASAR_SAMPLE_PAID_PAYMENT_ID') ?: $payment->id;
+
+ // act
+ $refunded = $moyasar->payment()->refund($paymentId, 5000);
+
+ // assert
+ expect($refunded)->toBeInstanceOf(PaymentDTO::class)
+ ->and($refunded->id)->toBe($paymentId)
+ ->and($refunded->status)->toBe('refunded');
+})->skip(
+ fn () => getenv('REAL_TESTS') === 'true' && empty(getenv('MOYASAR_SAMPLE_PAID_PAYMENT_ID')),
+ 'MOYASAR_SAMPLE_PAID_PAYMENT_ID is required for real refund tests'
+);
diff --git a/tests/Feature/Payment/UpdatePaymentTest.php b/tests/Feature/Payment/UpdatePaymentTest.php
new file mode 100644
index 0000000..bd3077b
--- /dev/null
+++ b/tests/Feature/Payment/UpdatePaymentTest.php
@@ -0,0 +1,25 @@
+ validateEnvIsSet());
+
+it('updates a payment metadata', function () {
+ // arrange
+ $moyasar = MoyasarInitializer::getInstance()->getMoyasar();
+
+ $payment = $moyasar->payment()->create(CreatePaymentDTO::fromArray(mockCreatePaymentDTO()));
+
+ // act
+ $updated = $moyasar->payment()->update($payment->id, new UpdatePaymentDTO(
+ metadata: ['order_id' => '1234'],
+ ));
+
+ // assert
+ expect($updated)->toBeInstanceOf(PaymentDTO::class)
+ ->and($updated->id)->toBe($payment->id)
+ ->and($updated->metadata)->toBe(['order_id' => '1234']);
+});
diff --git a/tests/Feature/Payment/VoidPaymentTest.php b/tests/Feature/Payment/VoidPaymentTest.php
new file mode 100644
index 0000000..f7b109f
--- /dev/null
+++ b/tests/Feature/Payment/VoidPaymentTest.php
@@ -0,0 +1,29 @@
+ validateEnvIsSet());
+
+it('voids a payment', function () {
+ // arrange
+ $moyasar = MoyasarInitializer::getInstance()->getMoyasar();
+
+ $payment = $moyasar->payment()->create(CreatePaymentDTO::fromArray(mockCreatePaymentDTO()));
+
+ $paymentId = getenv('MOYASAR_SAMPLE_AUTHORIZED_PAYMENT_ID') ?: $payment->id;
+
+ // act
+ $voided = $moyasar->payment()->void($paymentId);
+
+ // assert
+ expect($voided)->toBeInstanceOf(PaymentDTO::class)
+ ->and($voided->id)->toBe($paymentId)
+ ->and($voided->status)->toBe('voided')
+ ->and($voided->voidedAt)->not->toBeNull()
+ ->and($voided->captured)->toBe(0);
+})->skip(
+ fn () => getenv('REAL_TESTS') === 'true' && empty(getenv('MOYASAR_SAMPLE_AUTHORIZED_PAYMENT_ID')),
+ 'MOYASAR_SAMPLE_AUTHORIZED_PAYMENT_ID is required for real void tests'
+);
diff --git a/tests/Pest.php b/tests/Pest.php
index 6a1d083..834db14 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -101,3 +101,58 @@ function mockInvoiceDTO(array $overrides = []): array
...$overrides
];
}
+
+function mockCreatePaymentDTO(array $overrides = []): array
+{
+ return [
+ 'amount' => 10000,
+ 'currency' => 'SAR',
+ 'description' => 'Test payment',
+ 'callback_url' => 'https://example.com/webhook',
+ 'source' => [
+ 'type' => 'creditcard',
+ 'name' => 'John Doe',
+ 'number' => '4111111111111111',
+ 'month' => 12,
+ 'year' => 2030,
+ 'cvc' => 123,
+ ],
+ ...$overrides
+ ];
+}
+
+function mockPaymentDTO(array $overrides = []): array
+{
+ return [
+ 'id' => 'pay_1',
+ 'status' => 'paid',
+ 'amount' => 10000,
+ 'fee' => 50,
+ 'currency' => 'SAR',
+ 'refunded' => 0,
+ 'refunded_at' => null,
+ 'captured' => 10000,
+ 'captured_at' => '2024-01-15T12:00:00Z',
+ 'voided_at' => null,
+ 'description' => 'Test payment',
+ 'amount_format' => '100.00 SAR',
+ 'fee_format' => '0.50 SAR',
+ 'refunded_format' => '0.00 SAR',
+ 'captured_format' => '100.00 SAR',
+ 'invoice_id' => null,
+ 'ip' => '127.0.0.1',
+ 'callback_url' => null,
+ 'created_at' => '2024-01-15T10:00:00Z',
+ 'updated_at' => '2024-01-15T12:00:00Z',
+ 'metadata' => [],
+ 'source' => [
+ 'type' => 'creditcard',
+ 'company' => 'visa',
+ 'name' => 'John Doe',
+ 'number' => '**** **** **** 1111',
+ 'message' => null,
+ ],
+ 'given_id' => null,
+ ...$overrides
+ ];
+}