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 + ]; +}