From 64f4ff8af87d4d9b02dd70f488af065ddda9770e Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sun, 10 May 2026 19:59:39 +0300 Subject: [PATCH 01/11] created the new invoice dto, renamed the class, replaced factory method, added currency enum and helper to calculate decimal points --- composer.lock | 51 ++++--- src/Invoice/Shared/Const/Currency.php | 195 ++++++++++++++++++++++++++ src/Invoice/Shared/Models/Invoice.php | 105 ++++++++++++++ 3 files changed, 325 insertions(+), 26 deletions(-) create mode 100644 src/Invoice/Shared/Const/Currency.php create mode 100644 src/Invoice/Shared/Models/Invoice.php diff --git a/composer.lock b/composer.lock index c581aa1..a75a3b6 100644 --- a/composer.lock +++ b/composer.lock @@ -672,16 +672,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -694,7 +694,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -719,7 +719,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -730,12 +730,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" } ], "packages-dev": [ @@ -4236,20 +4240,19 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.37.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -4297,7 +4300,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -4308,16 +4311,12 @@ "url": "https://github.com/fabpot", "type": "github" }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", @@ -4386,16 +4385,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -4413,7 +4412,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -4449,7 +4448,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -4469,7 +4468,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", @@ -4744,5 +4743,5 @@ "platform-overrides": { "php": "8.3.0" }, - "plugin-api-version": "2.9.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Invoice/Shared/Const/Currency.php b/src/Invoice/Shared/Const/Currency.php new file mode 100644 index 0000000..786b4be --- /dev/null +++ b/src/Invoice/Shared/Const/Currency.php @@ -0,0 +1,195 @@ + 3, + Currency::BIF, Currency::CLP, Currency::DJF, Currency::GNF, Currency::ISK, Currency::JPY, Currency::KMF, Currency::KRW, Currency::PYG, Currency::RWF, Currency::UGX, Currency::UYI, Currency::VND, Currency::VUV, Currency::XAF, Currency::XOF, Currency::XPF => 0, + default => 2 + }; + } +} diff --git a/src/Invoice/Shared/Models/Invoice.php b/src/Invoice/Shared/Models/Invoice.php new file mode 100644 index 0000000..a918782 --- /dev/null +++ b/src/Invoice/Shared/Models/Invoice.php @@ -0,0 +1,105 @@ + + * @phpstan-type InvoiceArray array{ + * amount: float, + * currency: Currency, + * description: string, + * callback_url?: ?string, + * success_url?: ?string, + * back_url?: ?string, + * expired_at?: ?DateTime, + * metadata?: array + *} + */ +final class Invoice +{ + public function __construct( + private float $amount, + private Currency $currency, + private string $description, + private ?string $callbackUrl = null, + private ?string $successUrl = null, + private ?string $backUrl = null, + private ?DateTime $expiredAt = null, + private ?array $metadata = null, + ) { + } + + public static function create( + float $amount, + Currency $currency, + string $description, + ?string $callbackUrl = null, + ?string $successUrl = null, + ?string $backUrl = null, + ?DateTime $expiredAt = null, + ?array $metadata = null, + ): self { + return new self($amount, $currency, $description, $callbackUrl, $successUrl, $backUrl, $expiredAt, $metadata); + } + + /** + * @return InvoiceArray + */ + public function toRequestArray(): array + { + return [ + 'amount' => $this->amount, + 'currency' => $this->currency, + 'description' => $this->description, + 'callback_url' => $this->callbackUrl, + 'success_url' => $this->successUrl, + 'back_url' => $this->backUrl, + 'expired_at' => $this->expiredAt, + 'metadata' => $this->metadata, + ]; + } + + // Getters + public function getAmount(): float + { + return $this->amount; + } + + public function getCurrency(): Currency + { + return $this->currency; + } + + public function getdescription(): string + { + return $this->description; + } + + public function getcallbackUrl(): ?string + { + return $this->callbackUrl; + } + + public function getsuccessUrl(): ?string + { + return $this->successUrl; + } + + public function getbackUrl(): ?string + { + return $this->backUrl; + } + + public function getexpiredAt(): ?DateTime + { + return $this->expiredAt; + } + + public function getmetadata(): ?array + { + return $this->metadata; + } +} From 93f661d206faaeff8d31217f6a3da1be1f35b999 Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sat, 23 May 2026 06:24:04 +0300 Subject: [PATCH 02/11] create invoice migrated to new architecture with tests --- .gitignore | 4 +- src/Invoice/APIs/CreateInvoiceRequest.php | 47 ---- src/Invoice/DTO/InvoiceDTO.php | 3 + .../Internal/Core/Const/InvoiceStatus.php | 15 ++ .../Internal/Core/Const/PaymentCardType.php | 11 + .../Core/Const/PaymentSourceCompany.php | 11 + .../Internal/Core/Const/PaymentSourceType.php | 11 + .../Internal/Core/Const/PaymentStatus.php | 15 ++ .../Core/Const/SplitRecipientType.php | 10 + .../Validators/Contract/BaseValidator.php | 162 ++++++++++++ .../Validators/InvoiceResponseValidator.php | 33 +++ .../InvoiceResponsePaymentValidator.php | 55 +++++ .../InvoiceApplePayPaymentSourceValidator.php | 14 ++ ...nvoiceCreditCardPaymentSourceValidator.php | 14 ++ .../InvoicePaymentSourceValidator.php | 27 ++ ...nvoiceSamsungPayPaymentSourceValidator.php | 14 ++ .../InvoiceStcPayPaymentSourceValidator.php | 10 + .../Split/InvoicePaymentSplitsValidator.php | 12 + .../Internal/Create/CreateInvoiceRequest.php | 65 +++++ .../{ => Internal}/InvoiceResource.php | 17 +- .../Requests/InvoiceRequest.php} | 29 ++- .../ApplePayPaymentSourceDetails.php | 130 ++++++++++ .../Contract/PaymentSourceDetails.php | 8 + .../CreditCardPaymentSourceDetails.php | 128 ++++++++++ .../Shared/DTOs/Responses/InvoicePayment.php | 232 ++++++++++++++++++ .../DTOs/Responses/InvoicePaymentSource.php | 38 +++ .../DTOs/Responses/InvoicePaymentSplit.php | 72 ++++++ .../Responses/InvoiceSuccessfulResponse.php | 157 ++++++++++++ .../SamsungPayPaymentSourceDetails.php | 130 ++++++++++ .../Responses/StcPayPaymentSourceDetails.php | 67 +++++ src/Moyasar.php | 3 +- src/{Invoice => }/Shared/Const/Currency.php | 0 src/Shared/DTOs/ApiResponse.php | 40 +++ tests/Config/MockResponses.php | 4 +- tests/Config/Samples/InvoiceSamples.php | 2 + tests/Feature/Invoice/CreateInvoiceTest.php | 21 +- tests/Pest.php | 2 +- 37 files changed, 1536 insertions(+), 77 deletions(-) delete mode 100644 src/Invoice/APIs/CreateInvoiceRequest.php create mode 100644 src/Invoice/Internal/Core/Const/InvoiceStatus.php create mode 100644 src/Invoice/Internal/Core/Const/PaymentCardType.php create mode 100644 src/Invoice/Internal/Core/Const/PaymentSourceCompany.php create mode 100644 src/Invoice/Internal/Core/Const/PaymentSourceType.php create mode 100644 src/Invoice/Internal/Core/Const/PaymentStatus.php create mode 100644 src/Invoice/Internal/Core/Const/SplitRecipientType.php create mode 100644 src/Invoice/Internal/Core/Validators/Contract/BaseValidator.php create mode 100644 src/Invoice/Internal/Core/Validators/InvoiceResponseValidator.php create mode 100644 src/Invoice/Internal/Core/Validators/Payment/InvoiceResponsePaymentValidator.php create mode 100644 src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceApplePayPaymentSourceValidator.php create mode 100644 src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceCreditCardPaymentSourceValidator.php create mode 100644 src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php create mode 100644 src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceSamsungPayPaymentSourceValidator.php create mode 100644 src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceStcPayPaymentSourceValidator.php create mode 100644 src/Invoice/Internal/Core/Validators/Split/InvoicePaymentSplitsValidator.php create mode 100644 src/Invoice/Internal/Create/CreateInvoiceRequest.php rename src/Invoice/{ => Internal}/InvoiceResource.php (71%) rename src/Invoice/Shared/{Models/Invoice.php => DTOs/Requests/InvoiceRequest.php} (75%) create mode 100644 src/Invoice/Shared/DTOs/Responses/ApplePayPaymentSourceDetails.php create mode 100644 src/Invoice/Shared/DTOs/Responses/Contract/PaymentSourceDetails.php create mode 100644 src/Invoice/Shared/DTOs/Responses/CreditCardPaymentSourceDetails.php create mode 100644 src/Invoice/Shared/DTOs/Responses/InvoicePayment.php create mode 100644 src/Invoice/Shared/DTOs/Responses/InvoicePaymentSource.php create mode 100644 src/Invoice/Shared/DTOs/Responses/InvoicePaymentSplit.php create mode 100644 src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php create mode 100644 src/Invoice/Shared/DTOs/Responses/SamsungPayPaymentSourceDetails.php create mode 100644 src/Invoice/Shared/DTOs/Responses/StcPayPaymentSourceDetails.php rename src/{Invoice => }/Shared/Const/Currency.php (100%) create mode 100644 src/Shared/DTOs/ApiResponse.php diff --git a/.gitignore b/.gitignore index f2be0b4..bb48f6e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ .vscode/ phpunit.xml .env -docs/pr/ +docs/ +tmp/ +.codex diff --git a/src/Invoice/APIs/CreateInvoiceRequest.php b/src/Invoice/APIs/CreateInvoiceRequest.php deleted file mode 100644 index a475022..0000000 --- a/src/Invoice/APIs/CreateInvoiceRequest.php +++ /dev/null @@ -1,47 +0,0 @@ - $this->createInvoiceDTO->amount, - 'currency' => $this->createInvoiceDTO->currency, - 'description' => $this->createInvoiceDTO->description, - 'callback_url' => $this->createInvoiceDTO->callbackUrl, - 'success_url' => $this->createInvoiceDTO->successUrl, - 'back_url' => $this->createInvoiceDTO->backUrl, - 'expired_at' => $this->createInvoiceDTO->expiredAt, - 'metadata' => $this->createInvoiceDTO->metadata, - ], fn (mixed $value): bool => $value !== null); - } - - public function createDtoFromResponse(Response $response): InvoiceDTO - { - return InvoiceDTO::fromResponse($response); - } -} diff --git a/src/Invoice/DTO/InvoiceDTO.php b/src/Invoice/DTO/InvoiceDTO.php index 6b054fb..b3794f5 100644 --- a/src/Invoice/DTO/InvoiceDTO.php +++ b/src/Invoice/DTO/InvoiceDTO.php @@ -5,6 +5,9 @@ use Saloon\Http\Response; use Saloon\Traits\Responses\HasResponse; +/** + * @deprecated + */ final class InvoiceDTO { use HasResponse; diff --git a/src/Invoice/Internal/Core/Const/InvoiceStatus.php b/src/Invoice/Internal/Core/Const/InvoiceStatus.php new file mode 100644 index 0000000..dfd0909 --- /dev/null +++ b/src/Invoice/Internal/Core/Const/InvoiceStatus.php @@ -0,0 +1,15 @@ + + */ + protected array $requiredEnums = []; + + /** + * An associative array contains the property name as key for optional enum value casts, and the Enum class string as value + * @var array + */ + protected array $optionalEnums = []; + + /** + * Required timestamps' props names + * @var string[] + */ + protected array $requiredTimestamps = []; + + /** + * Optional timestamps' props names + * @var string[] + */ + protected array $optionalTimestamps = []; + + public function __construct( + protected readonly array $subject, + /** + * @var string the name of the subject context, used for error messages + * @example response + * @example response.property[0] + */ + protected readonly string $context = 'response' + ) { + } + + public static function for(array $subject, string $context = 'response'): static + { + return new static($subject); + } + + /** + * @throws InvalidArgumentException on the first first violated validation rule + * @return void + */ + public function validate(): void + { + $this->validateMandatoryFields(); + $this->validateEnums(); + $this->validateTimeStamps(); + $this->runAdditionalValidations(); + } + + protected function validateMandatoryFields(): void + { + foreach ($this->requiredFields as $k) { + if (!\array_key_exists($k, $this->subject)) { + throw new BadMethodCallException("Cannot cast {$this->context} array to object, missing key `{$k}`"); + } + } + } + + protected function validateEnums(): void + { + $this->validateMandatoryEnumFields(); + $this->validateOptionalEnumFields(); + } + + protected function validateTimestamps(): void + { + $this->validateMandatoryTimestamps(); + $this->validateOptionalTimestamps(); + } + + protected function validateMandatoryEnumFields(): void + { + foreach ($this->requiredEnums as $prop => $class) { + $this->castEnumField($prop, $class); + } + } + + protected function validateOptionalEnumFields(): void + { + foreach ($this->optionalEnums as $prop => $class) { + if (\array_key_exists($prop, $this->subject) && !empty($this->subject[$prop])) { + $this->castEnumField($prop, $class); + } + } + } + + /** + * Casts a subject prop to an enum instance + * @param string $prop + * @param string $class + * @throws InvalidArgumentException + * @return void + */ + protected function castEnumField(string $prop, string $class) + { + try { + $class::from($this->subject[$prop]); + } catch (TypeError | ValueError $e) { + $msg = "{$this->context}.{$prop}: {$this->subject[$prop]} cannot be casted to a valid {$class}, supported statuses are: " . json_encode($class::cases()); + throw new InvalidArgumentException($msg); + } + } + + protected function validateMandatoryTimestamps(): void + { + foreach ($this->requiredTimestamps as $field) { + $this->castTimestamp($field); + } + } + + protected function validateOptionalTimestamps(): void + { + foreach ($this->optionalTimestamps as $field) { + if (\array_key_exists($field, $this->subject) && !empty($this->subject[$field])) { + $this->castTimestamp($field); + } + } + } + + protected function castTimestamp(string $key) + { + $timestamp = $this->subject[$key]; + echo $key, $timestamp, "\n"; + + try { + new DateTimeImmutable($timestamp); + } catch (DateMalformedStringException $e) { + $msg = "{$this->context}.{$key}: {$timestamp} cannot be casted to a valid DateTimeImmutable"; + throw new InvalidArgumentException($msg); + } + } + + /** + * Run extra validations besides required fields, enum casts, timstamps' casts + * @throws InvalidArgumentException at the first violated validation rule + * @return void + */ + protected function runAdditionalValidations(): void + { + } +} diff --git a/src/Invoice/Internal/Core/Validators/InvoiceResponseValidator.php b/src/Invoice/Internal/Core/Validators/InvoiceResponseValidator.php new file mode 100644 index 0000000..1490f4e --- /dev/null +++ b/src/Invoice/Internal/Core/Validators/InvoiceResponseValidator.php @@ -0,0 +1,33 @@ + InvoiceStatus::class, 'currency' => Currency::class]; + protected array $requiredTimestamps = ['created_at', 'updated_at']; + protected array $optionalTimestamps = ['expired_at']; + + #[Override] + protected function runAdditionalValidations(): void + { + if (!is_numeric($this->subject['amount'])) { + $msg = "{$this->context}.amount: {$this->subject['amount']} is not a number"; + throw new InvalidArgumentException($msg); + } + + if (\array_key_exists('payments', $this->subject)) { + foreach ($this->subject['payments'] as $i => $pay) { + InvoiceResponsePaymentValidator::for($pay, "{$this->context}.payments[{$i}]")->validate(); + } + } + } +} diff --git a/src/Invoice/Internal/Core/Validators/Payment/InvoiceResponsePaymentValidator.php b/src/Invoice/Internal/Core/Validators/Payment/InvoiceResponsePaymentValidator.php new file mode 100644 index 0000000..b925949 --- /dev/null +++ b/src/Invoice/Internal/Core/Validators/Payment/InvoiceResponsePaymentValidator.php @@ -0,0 +1,55 @@ + InvoiceStatus::class, + 'currency' => Currency::class + ]; + + protected array $requiredTimestamps = ['created_at', 'updated_at']; + + protected array $optionalTimestamps = ['refunded_at', 'captured_at', 'voided_at']; + + #[Override] + protected function runAdditionalValidations(): void + { + $this->validatePrimitiveTypes(); + $this->validateComposites(); + } + + private function validatePrimitiveTypes(): void + { + $numerics = ['amount', 'fee', 'refunded', 'captured']; + + foreach ($numerics as $n) { + if (!is_numeric($this->subject[$n])) { + $msg = "{$this->context}.{$n}: {$this->subject[$n]} is not a number"; + throw new InvalidArgumentException($msg); + } + } + } + + private function validateComposites(): void + { + // 'source', 'splits' + InvoicePaymentSourceValidator::for($this->subject['source'])->validate(); + + if (\array_key_exists('splits', $this->subject)) { + foreach ($this->subject['splits'] as $i => $sp) { + InvoicePaymentSplitsValidator::for($sp, "{$this->context}.splits[{$i}]")->validate(); + } + } + } +} diff --git a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceApplePayPaymentSourceValidator.php b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceApplePayPaymentSourceValidator.php new file mode 100644 index 0000000..930c77a --- /dev/null +++ b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceApplePayPaymentSourceValidator.php @@ -0,0 +1,14 @@ + PaymentSourceCompany::class]; + protected array $optionalEnums = ['issuer_card_type' => PaymentCardType::class]; +} diff --git a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceCreditCardPaymentSourceValidator.php b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceCreditCardPaymentSourceValidator.php new file mode 100644 index 0000000..2826cd8 --- /dev/null +++ b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceCreditCardPaymentSourceValidator.php @@ -0,0 +1,14 @@ + PaymentSourceCompany::class]; + protected array $optionalEnums = ['issuer_card_type' => PaymentCardType::class]; +} diff --git a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php new file mode 100644 index 0000000..92a2b25 --- /dev/null +++ b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php @@ -0,0 +1,27 @@ + PaymentSourceType::class]; + + #[Override] + protected function runAdditionalValidations(): void + { + $newContext = "{$this->context}[={$this->subject['type']}]"; + + match ($this->subject['type']) { + 'creditcard' => InvoiceCreditCardPaymentSourceValidator::for($this->subject, $newContext)->validate(), + 'applepay' => InvoiceApplePayPaymentSourceValidator::for($this->subject, $newContext)->validate(), + 'samsungpay' => InvoiceSamsungPayPaymentSourceValidator::for($this->subject, $newContext)->validate(), + 'stcpay' => InvoiceStcPayPaymentSourceValidator::for($this->subject, $newContext)->validate(), + }; + } +} diff --git a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceSamsungPayPaymentSourceValidator.php b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceSamsungPayPaymentSourceValidator.php new file mode 100644 index 0000000..fdc3837 --- /dev/null +++ b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceSamsungPayPaymentSourceValidator.php @@ -0,0 +1,14 @@ + PaymentSourceCompany::class]; + protected array $optionalEnums = ['issuer_card_type' => PaymentCardType::class]; +} diff --git a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceStcPayPaymentSourceValidator.php b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceStcPayPaymentSourceValidator.php new file mode 100644 index 0000000..60134ff --- /dev/null +++ b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceStcPayPaymentSourceValidator.php @@ -0,0 +1,10 @@ + SplitRecipientType::class]; +} diff --git a/src/Invoice/Internal/Create/CreateInvoiceRequest.php b/src/Invoice/Internal/Create/CreateInvoiceRequest.php new file mode 100644 index 0000000..59e0607 --- /dev/null +++ b/src/Invoice/Internal/Create/CreateInvoiceRequest.php @@ -0,0 +1,65 @@ +invoice, fn(mixed $value): bool => $value !== null); + } + + /** + * Summary of createDtoFromResponse + * @param Response $response + * @return ApiResponse + */ + public function createDtoFromResponse(Response $response): ApiResponse + { + $code = $response->status(); + $responseArr = $response->json(); + $responseStr = $response->body(); + $responseObject = null; + + if ($response->successful()) { + InvoiceResponseValidator::for($responseArr, 'response')->validate(); + $responseObject = InvoiceSuccessfulResponse::fromArray($responseArr); + } + + return new ApiResponse( + code: $code, + response: $responseObject, + responseAsString: $responseStr + ); + } +} diff --git a/src/Invoice/InvoiceResource.php b/src/Invoice/Internal/InvoiceResource.php similarity index 71% rename from src/Invoice/InvoiceResource.php rename to src/Invoice/Internal/InvoiceResource.php index 456aea0..ce9adbf 100644 --- a/src/Invoice/InvoiceResource.php +++ b/src/Invoice/Internal/InvoiceResource.php @@ -1,16 +1,17 @@ + */ + public function create(InvoiceRequest $invoice): ApiResponse { - return $this->connector->send(new CreateInvoiceRequest($dto))->dto(); + return $this->connector->send(new CreateInvoiceRequest($invoice->toRequestArray()))->dto(); } public function update(string $invoiceId, UpdateInvoiceDTO $dto): InvoiceDTO @@ -41,7 +46,7 @@ public function cancel(string $invoiceId): InvoiceDTO } /** - * @param CreateInvoiceDTO[] $invoices + * @param InvoiceRequest[] $invoices * @return array{invoices: InvoiceDTO[]} */ public function bulkCreate(array $invoices): array diff --git a/src/Invoice/Shared/Models/Invoice.php b/src/Invoice/Shared/DTOs/Requests/InvoiceRequest.php similarity index 75% rename from src/Invoice/Shared/Models/Invoice.php rename to src/Invoice/Shared/DTOs/Requests/InvoiceRequest.php index a918782..9916042 100644 --- a/src/Invoice/Shared/Models/Invoice.php +++ b/src/Invoice/Shared/DTOs/Requests/InvoiceRequest.php @@ -1,25 +1,26 @@ - * @phpstan-type InvoiceArray array{ - * amount: float, - * currency: Currency, + * @phpstan-type InvoiceRequestArray array{ + * amount: int, + * currency: string, * description: string, * callback_url?: ?string, * success_url?: ?string, * back_url?: ?string, - * expired_at?: ?DateTime, + * expired_at?: ?string, * metadata?: array *} */ -final class Invoice +final class InvoiceRequest { + private const string DEFAULT_DATETIME_FORMAT = 'Y-m-d\TH:i:s.v\Z'; + public function __construct( private float $amount, private Currency $currency, @@ -46,23 +47,27 @@ public static function create( } /** - * @return InvoiceArray + * @return InvoiceRequestArray */ public function toRequestArray(): array { + $amount = (int) ($this->amount * pow(10, Currency::minorUnitFor($this->currency))); + $currency = $this->currency->value; + $expiredAt = $this->expiredAt?->format(self::DEFAULT_DATETIME_FORMAT); + return [ - 'amount' => $this->amount, - 'currency' => $this->currency, + 'amount' => $amount, + 'currency' => $currency, 'description' => $this->description, 'callback_url' => $this->callbackUrl, 'success_url' => $this->successUrl, 'back_url' => $this->backUrl, - 'expired_at' => $this->expiredAt, + 'expired_at' => $expiredAt, 'metadata' => $this->metadata, ]; } - // Getters + // ==================== GETTERS ==================== // public function getAmount(): float { return $this->amount; diff --git a/src/Invoice/Shared/DTOs/Responses/ApplePayPaymentSourceDetails.php b/src/Invoice/Shared/DTOs/Responses/ApplePayPaymentSourceDetails.php new file mode 100644 index 0000000..8785796 --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/ApplePayPaymentSourceDetails.php @@ -0,0 +1,130 @@ +company; + } + + public function getNumber(): string + { + return $this->number; + } + + public function getGatewayId(): string + { + return $this->gatewayId; + } + + public function getReferenceNumber(): string + { + return $this->referenceNumber; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getToken(): string + { + return $this->token; + } + + public function getDpan(): ?string + { + return $this->dpan; + } + + public function getResponseCode(): ?string + { + return $this->responseCode; + } + + public function getAuthorizationCode(): ?string + { + return $this->authorizationCode; + } + + public function getIssuerName(): ?string + { + return $this->issuerName; + } + + public function getIssuerCountry(): ?string + { + return $this->issuerCountry; + } + + public function getIssuerCardType(): ?PaymentCardType + { + return $this->issuerCardType; + } + + public function getIssuerCardCategory(): ?string + { + return $this->issuerCardCategory; + } + + public function getName(): ?string + { + return $this->name; + } +} diff --git a/src/Invoice/Shared/DTOs/Responses/Contract/PaymentSourceDetails.php b/src/Invoice/Shared/DTOs/Responses/Contract/PaymentSourceDetails.php new file mode 100644 index 0000000..923083e --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/Contract/PaymentSourceDetails.php @@ -0,0 +1,8 @@ +company; + } + + public function getName(): string + { + return $this->name; + } + + public function getNumber(): string + { + return $this->number; + } + + public function getGatewayId(): string + { + return $this->gatewayId; + } + + public function getToken(): string + { + return $this->token; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getTransactionUrl(): string + { + return $this->transactionUrl; + } + + public function getReferenceNumber(): string + { + return $this->referenceNumber; + } + + public function getAuthorizationCode(): ?string + { + return $this->authorizationCode; + } + + public function getResponseCode(): ?string + { + return $this->responseCode; + } + + public function getIssuerName(): ?string + { + return $this->issuerName; + } + + public function getIssuerCountry(): ?string + { + return $this->issuerCountry; + } + + public function getIssuerCardType(): ?PaymentCardType + { + return $this->issuerCardType; + } + + public function getIssuerCardCategory(): ?string + { + return $this->issuerCardCategory; + } +} diff --git a/src/Invoice/Shared/DTOs/Responses/InvoicePayment.php b/src/Invoice/Shared/DTOs/Responses/InvoicePayment.php new file mode 100644 index 0000000..3481864 --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/InvoicePayment.php @@ -0,0 +1,232 @@ + */ + private ?array $metadata = null, + /** @var ?InvoicePaymentSplit[] */ + private ?array $splits = null, + ) { + } + + public static function fromArray(array $data): self + { + $status = PaymentStatus::from($data['status']); + $amount = (int) $data['amount']; + $fee = (int) $data['fee']; + $currency = Currency::from($data['currency']); + $refunded = (int) $data['refunded']; + $captured = (int) $data['captured']; + $refundedAt = (\array_key_exists('refunded_at', $data)) ? new DateTimeImmutable($data['refunded_at']) : null; + $capturedAt = (\array_key_exists('captured_at', $data)) ? new DateTimeImmutable($data['captured_at']) : null; + $voidedAt = (\array_key_exists('voided_at', $data)) ? new DateTimeImmutable($data['voided_at']) : null; + $createdAt = new DateTimeImmutable($data['created_at']); + $updatedAt = new DateTimeImmutable($data['updated_at']); + $metadata = $data['metadata'] ?? null; + $source = InvoicePaymentSource::fromArray($data['source']); + $splits = \array_map(InvoicePaymentSplit::fromArray(...), $data['splits'] ?? []); + + return new self( + id: $data['id'], + status: $status, + amount: $amount, + fee: $fee, + currency: $currency, + refunded: $refunded, + captured: $captured, + amountFormat: $data['amountFormat'], + feeFormat: $data['feeFormat'], + refundedFormat: $data['refundedFormat'], + capturedFormat: $data['capturedFormat'], + ip: $data['ip'], + source: $source, + createdAt: $createdAt, + updatedAt: $updatedAt, + refundedAt: $refundedAt, + capturedAt: $capturedAt, + voidedAt: $voidedAt, + description: $data['description'] ?? null, + invoiceId: $data['invoiceId'] ?? null, + callbackUrl: $data['callbackUrl'] ?? null, + metadata: $metadata, + splits: $splits, + ); + } + + public function getId(): string + { + return $this->id; + } + + public function getStatus(): PaymentStatus + { + return $this->status; + } + + public function getAmount(): int + { + return $this->amount; + } + + public function getAmountAsFloat(): float + { + $decimals = Currency::minorUnitFor($this->currency); + $divider = pow(10, $decimals); + return round($this->amount / $divider, $decimals); + } + + public function getFee(): int + { + return $this->fee; + } + + public function getFeeAsFloat(): float + { + $decimals = Currency::minorUnitFor($this->currency); + $divider = pow(10, $decimals); + return round($this->fee / $divider, $decimals); + } + + public function getCurrency(): Currency + { + return $this->currency; + } + + public function getRefunded(): int + { + return $this->refunded; + } + + public function getRefundedAsFloat(): float + { + $decimals = Currency::minorUnitFor($this->currency); + $divider = pow(10, $decimals); + return round($this->refunded / $divider, $decimals); + } + + public function getCaptured(): int + { + return $this->captured; + } + + public function getCapturedAsFloat(): float + { + $decimals = Currency::minorUnitFor($this->currency); + $divider = pow(10, $decimals); + return round($this->captured / $divider, $decimals); + } + + public function getAmountFormat(): string + { + return $this->amountFormat; + } + + public function getFeeFormat(): string + { + return $this->feeFormat; + } + + public function getRefundedFormat(): string + { + return $this->refundedFormat; + } + + public function getCapturedFormat(): string + { + return $this->capturedFormat; + } + + public function getIp(): string + { + return $this->ip; + } + + public function getSource(): InvoicePaymentSource + { + return $this->source; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeImmutable + { + return $this->updatedAt; + } + + public function getRefundedAt(): ?DateTimeImmutable + { + return $this->refundedAt; + } + + public function getCapturedAt(): ?DateTimeImmutable + { + return $this->capturedAt; + } + + public function getVoidedAt(): ?DateTimeImmutable + { + return $this->voidedAt; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function getInvoiceId(): ?string + { + return $this->invoiceId; + } + + public function getCallbackUrl(): ?string + { + return $this->callbackUrl; + } + + /** + * @return ?array + */ + public function getMetadata(): ?array + { + return $this->metadata; + } + + /** + * @return ?InvoicePaymentSplit[] + */ + public function getSplits(): ?array + { + return $this->splits; + } +} diff --git a/src/Invoice/Shared/DTOs/Responses/InvoicePaymentSource.php b/src/Invoice/Shared/DTOs/Responses/InvoicePaymentSource.php new file mode 100644 index 0000000..deb9cc5 --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/InvoicePaymentSource.php @@ -0,0 +1,38 @@ + CreditCardPaymentSourceDetails::from($data), + PaymentSourceType::APPLE_PAY => ApplePayPaymentSourceDetails::from($data), + PaymentSourceType::SAMSUNG_PAY => SamsungPayPaymentSourceDetails::from($data), + PaymentSourceType::STC_PAY => StcPayPaymentSourceDetails::from($data), + }; + + return new self($type, $details); + } + + public function getType(): PaymentSourceType + { + return $this->type; + } + + public function getPaymentSourceDetails(): PaymentSourceDetails + { + return $this->paymentSourceDetails; + } +} diff --git a/src/Invoice/Shared/DTOs/Responses/InvoicePaymentSplit.php b/src/Invoice/Shared/DTOs/Responses/InvoicePaymentSplit.php new file mode 100644 index 0000000..cc4a275 --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/InvoicePaymentSplit.php @@ -0,0 +1,72 @@ +amount; + } + + public function getRecipientType(): SplitRecipientType + { + return $this->recipientType; + } + + public function getRecipientId(): string + { + return $this->recipientId; + } + + public function getFeeSource(): bool + { + return $this->feeSource; + } + + public function getRefundable(): bool + { + return $this->refundable; + } + + public function getReference(): ?string + { + return $this->reference; + } + + public function getDescription(): ?string + { + return $this->description; + } +} diff --git a/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php b/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php new file mode 100644 index 0000000..ee76131 --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php @@ -0,0 +1,157 @@ + */ + private array $metadata = [], + /** @var InvoicePayment[] */ + private array $payments = [], + private ?string $callbackUrl = null, + private ?DateTimeImmutable $expiredAt = null, + private ?string $backUrl = null, + private ?string $successUrl = null, + ) { + } + + /** + * @param array $data validated response array + */ + public static function fromArray(array $data): self + { + $invoiceStatus = InvoiceStatus::from($data['status']); + $currency = Currency::from($data['currency']); + $createdAt = new DateTimeImmutable($data['created_at']); + $updatedAt = new DateTimeImmutable($data['updated_at']); + $expiredAt = (\array_key_exists('expired_at', $data) && !empty($data['expired_at'])) ? new DateTimeImmutable($data['expired_at']) : null; + $payments = \array_map(InvoicePayment::fromArray(...), $data['payments'] ?? []); + + return new self( + id: $data['id'], + status: $invoiceStatus, + amount: (int) $data['amount'], + currency: $currency, + description: $data['description'], + amountFormat: $data['amount_format'], + url: $data['url'], + createdAt: $createdAt, + updatedAt: $updatedAt, + logoUrl: $data['logo_url'] ?? null, + callbackUrl: $data['callback_url'] ?? null, + expiredAt: $expiredAt, + backUrl: $data['back_url'] ?? null, + successUrl: $data['success_url'] ?? null, + metadata: $data['metadata'] ?? [], + payments: $payments, + ); + } + + // Getters + public function getId(): string + { + return $this->id; + } + + public function getStatus(): InvoiceStatus + { + return $this->status; + } + + public function getAmount(): int + { + return $this->amount; + } + + public function getAmountAsFloat(): float + { + $decimals = Currency::minorUnitFor($this->currency); + $divider = pow(10, $decimals); + return round($this->amount / $divider, $decimals); + } + + public function getCurrency(): Currency + { + return $this->currency; + } + + public function getDescription(): string + { + return $this->description; + } + + public function getAmountFormat(): string + { + return $this->amountFormat; + } + + public function getUrl(): string + { + return $this->url; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeImmutable + { + return $this->updatedAt; + } + + public function getLogoUrl(): ?string + { + return $this->logoUrl; + } + + public function getMetadata(): array + { + return $this->metadata; + } + + public function getPayments(): array + { + return $this->payments; + } + + public function getCallbackUrl(): ?string + { + return $this->callbackUrl; + } + + public function getExpiredAt(): ?DateTimeImmutable + { + return $this->expiredAt; + } + + public function getBackUrl(): ?string + { + return $this->backUrl; + } + + public function getSuccessUrl(): ?string + { + return $this->successUrl; + } +} diff --git a/src/Invoice/Shared/DTOs/Responses/SamsungPayPaymentSourceDetails.php b/src/Invoice/Shared/DTOs/Responses/SamsungPayPaymentSourceDetails.php new file mode 100644 index 0000000..1e05933 --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/SamsungPayPaymentSourceDetails.php @@ -0,0 +1,130 @@ +company; + } + + public function getNumber(): string + { + return $this->number; + } + + public function getGatewayId(): string + { + return $this->gatewayId; + } + + public function getReferenceNumber(): string + { + return $this->referenceNumber; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getToken(): string + { + return $this->token; + } + + public function getDpan(): ?string + { + return $this->dpan; + } + + public function getResponseCode(): ?string + { + return $this->responseCode; + } + + public function getAuthorizationCode(): ?string + { + return $this->authorizationCode; + } + + public function getIssuerName(): ?string + { + return $this->issuerName; + } + + public function getIssuerCountry(): ?string + { + return $this->issuerCountry; + } + + public function getIssuerCardType(): ?PaymentCardType + { + return $this->issuerCardType; + } + + public function getIssuerCardCategory(): ?string + { + return $this->issuerCardCategory; + } + + public function getName(): ?string + { + return $this->name; + } +} diff --git a/src/Invoice/Shared/DTOs/Responses/StcPayPaymentSourceDetails.php b/src/Invoice/Shared/DTOs/Responses/StcPayPaymentSourceDetails.php new file mode 100644 index 0000000..62744b5 --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/StcPayPaymentSourceDetails.php @@ -0,0 +1,67 @@ +mobile; + } + + public function getReferenceNumber(): string + { + return $this->referenceNumber; + } + + public function getCashier(): ?string + { + return $this->cashier; + } + + public function getBranch(): ?string + { + return $this->branch; + } + + public function getTransactionUrl(): ?string + { + return $this->transactionUrl; + } + + public function getMessage(): ?string + { + return $this->message; + } +} diff --git a/src/Moyasar.php b/src/Moyasar.php index 8f2e249..694b92e 100644 --- a/src/Moyasar.php +++ b/src/Moyasar.php @@ -2,11 +2,12 @@ namespace HamodaDev\Moyasar; -use HamodaDev\Moyasar\Invoice\InvoiceResource; +use HamodaDev\Moyasar\Invoice\Internal\InvoiceResource; use HamodaDev\Moyasar\Payment\PaymentResource; use Saloon\Http\Auth\BasicAuthenticator; use Saloon\Http\Connector; +// TODO: make it singleton class Moyasar extends Connector { public function __construct( diff --git a/src/Invoice/Shared/Const/Currency.php b/src/Shared/Const/Currency.php similarity index 100% rename from src/Invoice/Shared/Const/Currency.php rename to src/Shared/Const/Currency.php diff --git a/src/Shared/DTOs/ApiResponse.php b/src/Shared/DTOs/ApiResponse.php new file mode 100644 index 0000000..390e10f --- /dev/null +++ b/src/Shared/DTOs/ApiResponse.php @@ -0,0 +1,40 @@ +code >= 200 && $this->code < 300; + } + + public function getCode(): int + { + return $this->code; + } + + public function getResponseAsString(): string + { + return $this->responseAsString; + } + + /** + * @return T|null + */ + public function getResponse(): ?object + { + return $this->response; + } +} diff --git a/tests/Config/MockResponses.php b/tests/Config/MockResponses.php index 5de949d..61971eb 100644 --- a/tests/Config/MockResponses.php +++ b/tests/Config/MockResponses.php @@ -2,14 +2,16 @@ namespace Tests\Config; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoiceSuccessfulResponse; use Saloon\Http\Faking\MockResponse; use Tests\Config\Samples\InvoiceSamples; 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\Invoice\Internal\Create\CreateInvoiceRequest; +use HamodaDev\Moyasar\Shared\DTOs\ApiResponse; class MockResponses { diff --git a/tests/Config/Samples/InvoiceSamples.php b/tests/Config/Samples/InvoiceSamples.php index 102bb84..a700465 100644 --- a/tests/Config/Samples/InvoiceSamples.php +++ b/tests/Config/Samples/InvoiceSamples.php @@ -16,6 +16,8 @@ class InvoiceSamples 'description' => 'test order', 'amount_format' => '10.00 USD', 'url' => self::MOCK_URL, + 'created_at' => '2026-05-12 11:46:52', + 'updated_at' => '2026-05-12 11:46:52', 'metadata' => [ 'order_id' => '1234' ], diff --git a/tests/Feature/Invoice/CreateInvoiceTest.php b/tests/Feature/Invoice/CreateInvoiceTest.php index 0b0e499..b589a9a 100644 --- a/tests/Feature/Invoice/CreateInvoiceTest.php +++ b/tests/Feature/Invoice/CreateInvoiceTest.php @@ -1,7 +1,11 @@ getMoyasar(); // act - $invoice = $moyasar->invoice()->create(new CreateInvoiceDTO( + $invoice = $moyasar->invoice()->create(new InvoiceRequest( amount: 1000, - currency: 'USD', + currency: Currency::USD, description: 'Order #1234', callbackUrl: 'https://example.com/webhooks/moyasar', )); // assert - expect($invoice)->toBeInstanceOf(InvoiceDTO::class) - ->and($invoice->id)->not->toBeEmpty() - ->and($invoice->status)->toBe('initiated') - ->and($invoice->amount)->toBe(1000) - ->and($invoice->currency)->toBe('USD') - ->and($invoice->url)->toStartWith('https://'); + $response = $invoice->getResponse(); + expect($response)->toBeInstanceOf(InvoiceSuccessfulResponse::class) + ->and($response->getId())->not->toBeEmpty() + ->and($response->getStatus())->toBe(InvoiceStatus::INITIATED) + ->and($response->getAmountAsFloat())->toBe(1000.0) + ->and($response->getCurrency())->toBe(Currency::USD) + ->and($response->getUrl())->toStartWith('https://'); }); it('Bulk creates invoices', function () { diff --git a/tests/Pest.php b/tests/Pest.php index 6a1d083..d1ecb17 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -29,7 +29,7 @@ */ uses() - ->beforeEach(fn () => MockClient::destroyGlobal()) + ->beforeEach(fn() => MockClient::destroyGlobal()) ->in(__DIR__); function validateEnvIsSet() From 0300680f71fb7f167b10e9ce357bacf9b0bff659 Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sat, 23 May 2026 06:39:10 +0300 Subject: [PATCH 03/11] php cs fixes --- .../Validators/Contract/BaseValidator.php | 7 +++++-- .../Validators/InvoiceResponseValidator.php | 13 ++++++++++++- .../InvoiceResponsePaymentValidator.php | 19 ++++++++++++++++++- .../InvoiceApplePayPaymentSourceValidator.php | 11 ++++++++++- ...nvoiceCreditCardPaymentSourceValidator.php | 13 ++++++++++++- .../InvoicePaymentSourceValidator.php | 1 - ...nvoiceSamsungPayPaymentSourceValidator.php | 11 ++++++++++- .../Responses/InvoiceSuccessfulResponse.php | 5 +++-- src/Shared/Const/Currency.php | 9 +++++++-- 9 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/Invoice/Internal/Core/Validators/Contract/BaseValidator.php b/src/Invoice/Internal/Core/Validators/Contract/BaseValidator.php index 1dd0c28..49d04f4 100644 --- a/src/Invoice/Internal/Core/Validators/Contract/BaseValidator.php +++ b/src/Invoice/Internal/Core/Validators/Contract/BaseValidator.php @@ -23,7 +23,8 @@ abstract class BaseValidator protected array $requiredEnums = []; /** - * An associative array contains the property name as key for optional enum value casts, and the Enum class string as value + * An associative array contains the property name as key for optional enum value casts, + * and the Enum class string as value * @var array */ protected array $optionalEnums = []; @@ -117,7 +118,9 @@ protected function castEnumField(string $prop, string $class) try { $class::from($this->subject[$prop]); } catch (TypeError | ValueError $e) { - $msg = "{$this->context}.{$prop}: {$this->subject[$prop]} cannot be casted to a valid {$class}, supported statuses are: " . json_encode($class::cases()); + $msg = "{$this->context}.{$prop}: {$this->subject[$prop]} cannot be casted to a valid {$class}," + . " supported statuses are: " + . json_encode($class::cases()); throw new InvalidArgumentException($msg); } } diff --git a/src/Invoice/Internal/Core/Validators/InvoiceResponseValidator.php b/src/Invoice/Internal/Core/Validators/InvoiceResponseValidator.php index 1490f4e..e72bf67 100644 --- a/src/Invoice/Internal/Core/Validators/InvoiceResponseValidator.php +++ b/src/Invoice/Internal/Core/Validators/InvoiceResponseValidator.php @@ -11,7 +11,18 @@ class InvoiceResponseValidator extends BaseValidator { - protected array $requiredFields = ['id', 'status', 'amount', 'currency', 'description', 'amount_format', 'url', 'created_at', 'updated_at']; + protected array $requiredFields = [ + 'id', + 'status', + 'amount', + 'currency', + 'description', + 'amount_format', + 'url', + 'created_at', + 'updated_at' + ]; + protected array $requiredEnums = ['status' => InvoiceStatus::class, 'currency' => Currency::class]; protected array $requiredTimestamps = ['created_at', 'updated_at']; protected array $optionalTimestamps = ['expired_at']; diff --git a/src/Invoice/Internal/Core/Validators/Payment/InvoiceResponsePaymentValidator.php b/src/Invoice/Internal/Core/Validators/Payment/InvoiceResponsePaymentValidator.php index b925949..7bf0348 100644 --- a/src/Invoice/Internal/Core/Validators/Payment/InvoiceResponsePaymentValidator.php +++ b/src/Invoice/Internal/Core/Validators/Payment/InvoiceResponsePaymentValidator.php @@ -12,7 +12,24 @@ class InvoiceResponsePaymentValidator extends BaseValidator { - protected array $requiredFields = ['id', 'status', 'amount', 'fee', 'currency', 'refunded', 'captured', 'amount_format', 'fee_format', 'refunded_format', 'captured_format', 'ip', 'created_at', 'updated_at', 'source']; + protected array $requiredFields = [ + 'id', + 'status', + 'amount', + 'fee', + 'currency', + 'refunded', + 'captured', + 'amount_format', + 'fee_format', + 'refunded_format', + 'captured_format', + 'ip', + 'created_at', + 'updated_at', + 'source' + ]; + protected array $requiredEnums = [ 'status' => InvoiceStatus::class, 'currency' => Currency::class diff --git a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceApplePayPaymentSourceValidator.php b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceApplePayPaymentSourceValidator.php index 930c77a..1d85262 100644 --- a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceApplePayPaymentSourceValidator.php +++ b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceApplePayPaymentSourceValidator.php @@ -8,7 +8,16 @@ class InvoiceApplePayPaymentSourceValidator extends BaseValidator { - protected array $requiredFields = ['type', 'company', 'number', 'gateway_id', 'token', 'message', 'reference_number']; + protected array $requiredFields = [ + 'type', + 'company', + 'number', + 'gateway_id', + 'token', + 'message', + 'reference_number' + ]; + protected array $requiredEnums = ['company' => PaymentSourceCompany::class]; protected array $optionalEnums = ['issuer_card_type' => PaymentCardType::class]; } diff --git a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceCreditCardPaymentSourceValidator.php b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceCreditCardPaymentSourceValidator.php index 2826cd8..a371bb9 100644 --- a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceCreditCardPaymentSourceValidator.php +++ b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceCreditCardPaymentSourceValidator.php @@ -8,7 +8,18 @@ class InvoiceCreditCardPaymentSourceValidator extends BaseValidator { - protected array $requiredFields = ['type', 'company', 'name', 'number', 'gateway_id', 'token', 'message', 'transaction_url', 'reference_number']; + protected array $requiredFields = [ + 'type', + 'company', + 'name', + 'number', + 'gateway_id', + 'token', + 'message', + 'transaction_url', + 'reference_number' + ]; + protected array $requiredEnums = ['company' => PaymentSourceCompany::class]; protected array $optionalEnums = ['issuer_card_type' => PaymentCardType::class]; } diff --git a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php index 92a2b25..422975b 100644 --- a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php +++ b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php @@ -6,7 +6,6 @@ use HamodaDev\Moyasar\Invoice\Internal\Core\Validators\Contract\BaseValidator; use Override; - class InvoicePaymentSourceValidator extends BaseValidator { protected array $requiredFields = ['type']; diff --git a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceSamsungPayPaymentSourceValidator.php b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceSamsungPayPaymentSourceValidator.php index fdc3837..665f6a1 100644 --- a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceSamsungPayPaymentSourceValidator.php +++ b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoiceSamsungPayPaymentSourceValidator.php @@ -8,7 +8,16 @@ class InvoiceSamsungPayPaymentSourceValidator extends BaseValidator { - protected array $requiredFields = ['type', 'company', 'number', 'gateway_id', 'token', 'message', 'reference_number']; + protected array $requiredFields = [ + 'type', + 'company', + 'number', + 'gateway_id', + 'token', + 'message', + 'reference_number' + ]; + protected array $requiredEnums = ['company' => PaymentSourceCompany::class]; protected array $optionalEnums = ['issuer_card_type' => PaymentCardType::class]; } diff --git a/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php b/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php index ee76131..312abf8 100644 --- a/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php +++ b/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php @@ -7,7 +7,6 @@ use HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoicePayment; use HamodaDev\Moyasar\Shared\Const\Currency; - final class InvoiceSuccessfulResponse { /** @@ -44,7 +43,9 @@ public static function fromArray(array $data): self $currency = Currency::from($data['currency']); $createdAt = new DateTimeImmutable($data['created_at']); $updatedAt = new DateTimeImmutable($data['updated_at']); - $expiredAt = (\array_key_exists('expired_at', $data) && !empty($data['expired_at'])) ? new DateTimeImmutable($data['expired_at']) : null; + $expiredAt = (\array_key_exists('expired_at', $data) && !empty($data['expired_at'])) + ? new DateTimeImmutable($data['expired_at']) + : null; $payments = \array_map(InvoicePayment::fromArray(...), $data['payments'] ?? []); return new self( diff --git a/src/Shared/Const/Currency.php b/src/Shared/Const/Currency.php index 786b4be..1a820f7 100644 --- a/src/Shared/Const/Currency.php +++ b/src/Shared/Const/Currency.php @@ -187,8 +187,13 @@ enum Currency: string public static function minorUnitFor(self $currency): int { return match ($currency) { - Currency::BHD, Currency::IQD, Currency::JOD, Currency::KWD, Currency::LYD, Currency::OMR, Currency::TND => 3, - Currency::BIF, Currency::CLP, Currency::DJF, Currency::GNF, Currency::ISK, Currency::JPY, Currency::KMF, Currency::KRW, Currency::PYG, Currency::RWF, Currency::UGX, Currency::UYI, Currency::VND, Currency::VUV, Currency::XAF, Currency::XOF, Currency::XPF => 0, + Currency::BHD, Currency::IQD, Currency::JOD, Currency::KWD, Currency::LYD, Currency::OMR, + Currency::TND => 3, + + Currency::BIF, Currency::CLP, Currency::DJF, Currency::GNF, Currency::ISK, Currency::JPY, Currency::KMF, + Currency::KRW, Currency::PYG, Currency::RWF, Currency::UGX, Currency::UYI, Currency::VND, Currency::VUV, + Currency::XAF, Currency::XOF, Currency::XPF => 0, + default => 2 }; } From aa10726c28ceafbbb1c7b2b30e75fff77964e826 Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sat, 23 May 2026 08:40:57 +0300 Subject: [PATCH 04/11] ref/better directory structure, feat/migrated bulk create with tests --- .../APIs/BulkCreateInvoicesRequest.php | 64 ---------------- .../Validators/Contract/BaseValidator.php | 6 +- .../InvoicePaymentSourceValidator.php | 1 + .../Create/BulkCreateInvoiceRequest.php | 74 +++++++++++++++++++ src/Invoice/Internal/InvoiceResource.php | 21 +++--- .../DTOs/Requests/BulkInvoiceRequest.php | 58 +++++++++++++++ .../Shared/DTOs/Requests/InvoiceRequest.php | 33 ++++++++- .../BulkInvoiceSuccessfulResponse.php | 45 +++++++++++ .../Responses/InvoiceSuccessfulResponse.php | 2 +- .../{ => Payment}/InvoicePayment.php | 4 +- .../Source}/ApplePayPaymentSourceDetails.php | 6 +- .../CreditCardPaymentSourceDetails.php | 6 +- .../Source}/InvoicePaymentSource.php | 3 +- .../SamsungPayPaymentSourceDetails.php | 6 +- .../Source}/StcPayPaymentSourceDetails.php | 6 +- .../Split}/InvoicePaymentSplit.php | 4 +- tests/Config/MockResponses.php | 4 +- tests/Config/Samples/InvoiceSamples.php | 5 ++ tests/Feature/Invoice/CreateInvoiceTest.php | 31 +++++--- 19 files changed, 271 insertions(+), 108 deletions(-) delete mode 100644 src/Invoice/APIs/BulkCreateInvoicesRequest.php create mode 100644 src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php create mode 100644 src/Invoice/Shared/DTOs/Requests/BulkInvoiceRequest.php create mode 100644 src/Invoice/Shared/DTOs/Responses/BulkInvoiceSuccessfulResponse.php rename src/Invoice/Shared/DTOs/Responses/{ => Payment}/InvoicePayment.php (96%) rename src/Invoice/Shared/DTOs/Responses/{ => Payment/Source}/ApplePayPaymentSourceDetails.php (95%) rename src/Invoice/Shared/DTOs/Responses/{ => Payment/Source}/CreditCardPaymentSourceDetails.php (94%) rename src/Invoice/Shared/DTOs/Responses/{ => Payment/Source}/InvoicePaymentSource.php (86%) rename src/Invoice/Shared/DTOs/Responses/{ => Payment/Source}/SamsungPayPaymentSourceDetails.php (95%) rename src/Invoice/Shared/DTOs/Responses/{ => Payment/Source}/StcPayPaymentSourceDetails.php (89%) rename src/Invoice/Shared/DTOs/Responses/{ => Payment/Split}/InvoicePaymentSplit.php (92%) diff --git a/src/Invoice/APIs/BulkCreateInvoicesRequest.php b/src/Invoice/APIs/BulkCreateInvoicesRequest.php deleted file mode 100644 index 60e6b44..0000000 --- a/src/Invoice/APIs/BulkCreateInvoicesRequest.php +++ /dev/null @@ -1,64 +0,0 @@ - $invoices - */ - public function __construct( - public readonly array $invoices, - ) { - } - - public function resolveEndpoint(): string - { - return '/invoices/bulk'; - } - - public function defaultBody(): array - { - return [ - 'invoices' => array_map( - fn(CreateInvoiceDTO $dto): array => array_filter([ - 'amount' => $dto->amount, - 'currency' => $dto->currency, - 'description' => $dto->description, - 'callback_url' => $dto->callbackUrl, - 'success_url' => $dto->successUrl, - 'back_url' => $dto->backUrl, - 'expired_at' => $dto->expiredAt, - 'metadata' => $dto->metadata, - ], fn(mixed $value): bool => $value !== null), - $this->invoices, - ), - ]; - } - - /** - * @param Response $response - * @return array{invoices: InvoiceDTO[]} - */ - public function createDtoFromResponse(Response $response): array - { - $data = []; - $data['invoices'] = array_map( - fn(array $invoice): InvoiceDTO => InvoiceDTO::fromArray($invoice), - $response->json('invoices', []) - ); - return $data; - } -} diff --git a/src/Invoice/Internal/Core/Validators/Contract/BaseValidator.php b/src/Invoice/Internal/Core/Validators/Contract/BaseValidator.php index 49d04f4..73474c7 100644 --- a/src/Invoice/Internal/Core/Validators/Contract/BaseValidator.php +++ b/src/Invoice/Internal/Core/Validators/Contract/BaseValidator.php @@ -9,7 +9,7 @@ use TypeError; use ValueError; -abstract class BaseValidator +class BaseValidator { /** * @var string[] @@ -52,9 +52,9 @@ public function __construct( ) { } - public static function for(array $subject, string $context = 'response'): static + public static function for(array $subject, string $context = 'response'): self { - return new static($subject); + return new self($subject, $context); } /** diff --git a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php index 422975b..f4fd59e 100644 --- a/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php +++ b/src/Invoice/Internal/Core/Validators/PaymentSource/InvoicePaymentSourceValidator.php @@ -21,6 +21,7 @@ protected function runAdditionalValidations(): void 'applepay' => InvoiceApplePayPaymentSourceValidator::for($this->subject, $newContext)->validate(), 'samsungpay' => InvoiceSamsungPayPaymentSourceValidator::for($this->subject, $newContext)->validate(), 'stcpay' => InvoiceStcPayPaymentSourceValidator::for($this->subject, $newContext)->validate(), + default => true, }; } } diff --git a/src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php b/src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php new file mode 100644 index 0000000..968cb32 --- /dev/null +++ b/src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php @@ -0,0 +1,74 @@ +invoices, fn(mixed $value): bool => $value !== null); + } + + /** + * Summary of createDtoFromResponse + * @param Response $response + * @return ApiResponse + */ + public function createDtoFromResponse(Response $response): ApiResponse + { + $code = $response->status(); + $responseArr = $response->json(); + $responseStr = $response->body(); + + /** + * @var InvoiceSuccessfulResponse[] + */ + $singleInvoiceResponses = []; + + if ($response->successful() && \array_key_exists('invoices', $responseArr)) { + foreach ($responseArr['invoices'] as $key => $invResponse) { + InvoiceResponseValidator::for($invResponse, "response.invoices[{$key}]")->validate(); + $singleInvoiceResponses[] = InvoiceSuccessfulResponse::fromArray($invResponse); + } + } + + $responseObj = BulkInvoiceSuccessfulResponse::fromArrayOfObjects($singleInvoiceResponses); + + return new ApiResponse( + code: $code, + response: $responseObj, + responseAsString: $responseStr + ); + } +} diff --git a/src/Invoice/Internal/InvoiceResource.php b/src/Invoice/Internal/InvoiceResource.php index ce9adbf..2886520 100644 --- a/src/Invoice/Internal/InvoiceResource.php +++ b/src/Invoice/Internal/InvoiceResource.php @@ -2,14 +2,15 @@ namespace HamodaDev\Moyasar\Invoice\Internal; -use HamodaDev\Moyasar\Invoice\APIs\BulkCreateInvoicesRequest; use HamodaDev\Moyasar\Invoice\APIs\CancelInvoiceRequest; use HamodaDev\Moyasar\Invoice\APIs\GetInvoiceRequest; use HamodaDev\Moyasar\Invoice\APIs\ListInvoicesRequest; use HamodaDev\Moyasar\Invoice\APIs\UpdateInvoiceRequest; use HamodaDev\Moyasar\Invoice\DTO\InvoiceDTO; use HamodaDev\Moyasar\Invoice\DTO\UpdateInvoiceDTO; +use HamodaDev\Moyasar\Invoice\Internal\Create\BulkCreateInvoiceRequest; use HamodaDev\Moyasar\Invoice\Internal\Create\CreateInvoiceRequest; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\BulkInvoiceRequest; use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\InvoiceRequest; use HamodaDev\Moyasar\Shared\DTOs\ApiResponse; use Saloon\Http\BaseResource; @@ -35,6 +36,15 @@ public function create(InvoiceRequest $invoice): ApiResponse return $this->connector->send(new CreateInvoiceRequest($invoice->toRequestArray()))->dto(); } + /** + * @param BulkInvoiceRequest $invoices + * @return ApiResponse<\HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\BulkInvoiceSuccessfulResponse> + */ + public function bulkCreate(BulkInvoiceRequest $invoices): ApiResponse + { + return $this->connector->send(new BulkCreateInvoiceRequest($invoices->toRequestArray()))->dto(); + } + public function update(string $invoiceId, UpdateInvoiceDTO $dto): InvoiceDTO { return $this->connector->send(new UpdateInvoiceRequest($invoiceId, $dto))->dto(); @@ -44,13 +54,4 @@ public function cancel(string $invoiceId): InvoiceDTO { return $this->connector->send(new CancelInvoiceRequest($invoiceId))->dto(); } - - /** - * @param InvoiceRequest[] $invoices - * @return array{invoices: InvoiceDTO[]} - */ - public function bulkCreate(array $invoices): array - { - return $this->connector->send(new BulkCreateInvoicesRequest($invoices))->dto(); - } } diff --git a/src/Invoice/Shared/DTOs/Requests/BulkInvoiceRequest.php b/src/Invoice/Shared/DTOs/Requests/BulkInvoiceRequest.php new file mode 100644 index 0000000..02e3c12 --- /dev/null +++ b/src/Invoice/Shared/DTOs/Requests/BulkInvoiceRequest.php @@ -0,0 +1,58 @@ + $inv) { + if (!($inv instanceof InvoiceRequest)) { + throw new InvalidArgumentException("Invalid Invoice at index {$i}"); + } + $result[] = $inv; + } + + return new self($result); + } + + /** + * @return InvoiceRequestArray[] + */ + public function toRequestArray(): array + { + $invoices = []; + foreach ($this->invoices as $inv) { + $invoices[] = $inv->toRequestArray(); + } + + return ['invoices' => $invoices]; + } + + /** + * @return InvoiceRequest[] + */ + public function getInvoices(): array + { + return $this->invoices; + } +} diff --git a/src/Invoice/Shared/DTOs/Requests/InvoiceRequest.php b/src/Invoice/Shared/DTOs/Requests/InvoiceRequest.php index 9916042..af2eb48 100644 --- a/src/Invoice/Shared/DTOs/Requests/InvoiceRequest.php +++ b/src/Invoice/Shared/DTOs/Requests/InvoiceRequest.php @@ -4,6 +4,7 @@ use DateTime; use HamodaDev\Moyasar\Shared\Const\Currency; +use InvalidArgumentException; /** * @phpstan-type InvoiceRequestArray array{ @@ -43,7 +44,37 @@ public static function create( ?DateTime $expiredAt = null, ?array $metadata = null, ): self { - return new self($amount, $currency, $description, $callbackUrl, $successUrl, $backUrl, $expiredAt, $metadata); + return new self( + amount: $amount, + currency: $currency, + description: $description, + callbackUrl: $callbackUrl, + successUrl: $successUrl, + backUrl: $backUrl, + expiredAt: $expiredAt, + metadata: $metadata + ); + } + + public static function createFromArray(array $data): self + { + $mandatoryFields = ['amount', 'currency', 'description']; + foreach ($mandatoryFields as $field) { + if (empty($data[$field])) { + throw new InvalidArgumentException("Field {$field} cannot be empty"); + } + } + + return new self( + amount: $data['amount'], + currency: $data['currency'], + description: $data['description'], + callbackUrl: $data['callbackUrl'] ?? null, + successUrl: $data['successUrl'] ?? null, + backUrl: $data['backUrl'] ?? null, + expiredAt: $data['expiredAt'] ?? null, + metadata: $data['metadata'] ?? null, + ); } /** diff --git a/src/Invoice/Shared/DTOs/Responses/BulkInvoiceSuccessfulResponse.php b/src/Invoice/Shared/DTOs/Responses/BulkInvoiceSuccessfulResponse.php new file mode 100644 index 0000000..6bc11c8 --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/BulkInvoiceSuccessfulResponse.php @@ -0,0 +1,45 @@ +> $data + * @return self + */ + public static function fromArrayOfArrays(array $data): self + { + $result = []; + foreach ($data as $inv) { + $result[] = InvoiceSuccessfulResponse::fromArray($inv); + } + + return new self($result); + } + + /** + * @return InvoiceSuccessfulResponse[] + */ + public function getInvoices(): array + { + return $this->invoices; + } +} diff --git a/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php b/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php index 312abf8..07dc12f 100644 --- a/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php +++ b/src/Invoice/Shared/DTOs/Responses/InvoiceSuccessfulResponse.php @@ -4,7 +4,7 @@ use DateTimeImmutable; use HamodaDev\Moyasar\Invoice\Internal\Core\Const\InvoiceStatus; -use HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoicePayment; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\Payment\InvoicePayment; use HamodaDev\Moyasar\Shared\Const\Currency; final class InvoiceSuccessfulResponse diff --git a/src/Invoice/Shared/DTOs/Responses/InvoicePayment.php b/src/Invoice/Shared/DTOs/Responses/Payment/InvoicePayment.php similarity index 96% rename from src/Invoice/Shared/DTOs/Responses/InvoicePayment.php rename to src/Invoice/Shared/DTOs/Responses/Payment/InvoicePayment.php index 3481864..6e727c5 100644 --- a/src/Invoice/Shared/DTOs/Responses/InvoicePayment.php +++ b/src/Invoice/Shared/DTOs/Responses/Payment/InvoicePayment.php @@ -1,9 +1,11 @@ MockResponse::make(body: InvoiceSamples::TEST_INVOICE), - BulkCreateInvoicesRequest::class => MockResponse::make(body: [ + BulkCreateInvoiceRequest::class => MockResponse::make(body: [ 'invoices' => [ InvoiceSamples::TEST_INVOICE, InvoiceSamples::TEST_INVOICE_2, diff --git a/tests/Config/Samples/InvoiceSamples.php b/tests/Config/Samples/InvoiceSamples.php index a700465..a0f859f 100644 --- a/tests/Config/Samples/InvoiceSamples.php +++ b/tests/Config/Samples/InvoiceSamples.php @@ -46,6 +46,11 @@ class InvoiceSamples 'description' => 'test order 22', 'amount_format' => '10.00 SAR', 'url' => self::MOCK_URL, + 'created_at' => '2026-05-10 11:46:52', + 'updated_at' => '2026-05-10 11:46:52', + 'metadata' => [ + 'order_id' => '5678' + ], ]; /** @var array */ diff --git a/tests/Feature/Invoice/CreateInvoiceTest.php b/tests/Feature/Invoice/CreateInvoiceTest.php index b589a9a..bf535fe 100644 --- a/tests/Feature/Invoice/CreateInvoiceTest.php +++ b/tests/Feature/Invoice/CreateInvoiceTest.php @@ -1,8 +1,7 @@ invoice()->create(new InvoiceRequest( - amount: 1000, + amount: 10, currency: Currency::USD, description: 'Order #1234', callbackUrl: 'https://example.com/webhooks/moyasar', @@ -28,7 +27,7 @@ expect($response)->toBeInstanceOf(InvoiceSuccessfulResponse::class) ->and($response->getId())->not->toBeEmpty() ->and($response->getStatus())->toBe(InvoiceStatus::INITIATED) - ->and($response->getAmountAsFloat())->toBe(1000.0) + ->and($response->getAmountAsFloat())->toBe(10.0) ->and($response->getCurrency())->toBe(Currency::USD) ->and($response->getUrl())->toStartWith('https://'); }); @@ -38,14 +37,24 @@ $moyasar = MoyasarInitializer::getInstance()->getMoyasar(); // act - $invoices = $moyasar->invoice()->bulkCreate([ - CreateInvoiceDTO::fromArray(mockCreateInvoiceDTO(['amount' => 69_00, 'currency' => 'USD'])), - CreateInvoiceDTO::fromArray(mockCreateInvoiceDTO(['amount' => 70_00, 'currency' => 'EGP'])), + $request = BulkInvoiceRequest::create([ + new InvoiceRequest( + amount: 69, + currency: Currency::USD, + description: 'Order #1111' + ), + new InvoiceRequest( + amount: 70, + currency: Currency::EGP, + description: 'Order #2222' + ) ]); + $invoices = $moyasar->invoice()->bulkCreate($request)->getResponse()?->getInvoices(); + // assert - expect($invoices['invoices'])->toBeArray()->toHaveCount(2) - ->and($invoices['invoices'][0])->toBeInstanceOf(InvoiceDTO::class) - ->and($invoices['invoices'][0]->id)->not->toBeEmpty() - ->and($invoices['invoices'][1]->id)->not->toBeEmpty(); + expect($invoices)->toBeArray()->toHaveCount(2) + ->and($invoices[0])->toBeInstanceOf(InvoiceSuccessfulResponse::class) + ->and($invoices[0]->getId())->not->toBeEmpty() + ->and($invoices[1]->getId())->not->toBeEmpty(); }); From cf5eb6747c4c3da0a3632ae2547f09bdfeb716fb Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sat, 23 May 2026 20:19:49 +0300 Subject: [PATCH 05/11] get and list invoice done with tests --- src/Invoice/APIs/GetInvoiceRequest.php | 28 ------- src/Invoice/APIs/ListInvoicesRequest.php | 26 ------- .../APIs/Pagination/ListInvoicesPaginator.php | 26 ------- .../PaginationMetadataValidator.php | 23 ++++++ .../Create/BulkCreateInvoiceRequest.php | 6 +- .../Internal/Create/CreateInvoiceRequest.php | 6 +- .../Internal/Get/GetInvoiceRequest.php | 49 +++++++++++++ .../Internal/Get/ListInvoiceRequest.php | 73 +++++++++++++++++++ src/Invoice/Internal/InvoiceResource.php | 31 +++++--- ...st.php => BulkCreateInvoiceRequestDto.php} | 18 ++--- ...equest.php => CreateInvoiceRequestDto.php} | 6 +- .../DTOs/Requests/ListInvoiceRequestDto.php | 53 ++++++++++++++ .../ListInvoiceSuccessfulResponse.php | 54 ++++++++++++++ .../Responses/Paginator/ResponsePaginator.php | 51 +++++++++++++ tests/Config/MockResponses.php | 14 ++-- tests/Config/Samples/InvoiceSamples.php | 5 ++ tests/Feature/Invoice/CreateInvoiceTest.php | 12 +-- tests/Feature/Invoice/GetInvoiceTest.php | 13 ++-- 18 files changed, 368 insertions(+), 126 deletions(-) delete mode 100644 src/Invoice/APIs/GetInvoiceRequest.php delete mode 100644 src/Invoice/APIs/ListInvoicesRequest.php delete mode 100644 src/Invoice/APIs/Pagination/ListInvoicesPaginator.php create mode 100644 src/Invoice/Internal/Core/Validators/Pagination/PaginationMetadataValidator.php create mode 100644 src/Invoice/Internal/Get/GetInvoiceRequest.php create mode 100644 src/Invoice/Internal/Get/ListInvoiceRequest.php rename src/Invoice/Shared/DTOs/Requests/{BulkInvoiceRequest.php => BulkCreateInvoiceRequestDto.php} (64%) rename src/Invoice/Shared/DTOs/Requests/{InvoiceRequest.php => CreateInvoiceRequestDto.php} (96%) create mode 100644 src/Invoice/Shared/DTOs/Requests/ListInvoiceRequestDto.php create mode 100644 src/Invoice/Shared/DTOs/Responses/ListInvoiceSuccessfulResponse.php create mode 100644 src/Invoice/Shared/DTOs/Responses/Paginator/ResponsePaginator.php diff --git a/src/Invoice/APIs/GetInvoiceRequest.php b/src/Invoice/APIs/GetInvoiceRequest.php deleted file mode 100644 index 5fe4fbf..0000000 --- a/src/Invoice/APIs/GetInvoiceRequest.php +++ /dev/null @@ -1,28 +0,0 @@ -invoiceId}"; - } - - public function createDtoFromResponse(Response $response): InvoiceDTO - { - return InvoiceDTO::fromResponse($response); - } -} diff --git a/src/Invoice/APIs/ListInvoicesRequest.php b/src/Invoice/APIs/ListInvoicesRequest.php deleted file mode 100644 index b96434f..0000000 --- a/src/Invoice/APIs/ListInvoicesRequest.php +++ /dev/null @@ -1,26 +0,0 @@ -json('meta.next_page') === null; - } - - protected function getPageItems(Response $response, Request $request): array - { - $invoices = $response->json('invoices', []); - - return array_map( - fn(array $invoice): InvoiceDTO => InvoiceDTO::fromArray($invoice), - $invoices, - ); - } -} diff --git a/src/Invoice/Internal/Core/Validators/Pagination/PaginationMetadataValidator.php b/src/Invoice/Internal/Core/Validators/Pagination/PaginationMetadataValidator.php new file mode 100644 index 0000000..2dfc362 --- /dev/null +++ b/src/Invoice/Internal/Core/Validators/Pagination/PaginationMetadataValidator.php @@ -0,0 +1,23 @@ +requiredFields as $f) { + if (!is_numeric($this->subject[$f])) { + $msg = "{$this->context}.{$f}: {$this->subject[$f]} is not a number"; + throw new InvalidArgumentException($msg); + } + } + } +} diff --git a/src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php b/src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php index 968cb32..ffd3832 100644 --- a/src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php +++ b/src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php @@ -13,7 +13,7 @@ use Saloon\Traits\Body\HasJsonBody; /** - * @phpstan-import-type InvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\InvoiceRequest + * @phpstan-import-type CreateInvoiceRequestDtoArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\InvoiceRequest */ class BulkCreateInvoiceRequest extends Request implements HasBody { @@ -22,7 +22,7 @@ class BulkCreateInvoiceRequest extends Request implements HasBody protected Method $method = Method::POST; public function __construct( - /** @var InvoiceRequest[] request */ + /** @var CreateInvoiceRequestDto[] request */ public readonly array $invoices, ) { } @@ -33,7 +33,7 @@ public function resolveEndpoint(): string } /** - * @return InvoiceRequestArray + * @return CreateInvoiceRequestDtoArray */ public function defaultBody(): array { diff --git a/src/Invoice/Internal/Create/CreateInvoiceRequest.php b/src/Invoice/Internal/Create/CreateInvoiceRequest.php index 59e0607..f70d34b 100644 --- a/src/Invoice/Internal/Create/CreateInvoiceRequest.php +++ b/src/Invoice/Internal/Create/CreateInvoiceRequest.php @@ -12,7 +12,7 @@ use Saloon\Traits\Body\HasJsonBody; /** - * @phpstan-import-type InvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\InvoiceRequest + * @phpstan-import-type CreateInvoiceRequestDtoArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\InvoiceRequest */ class CreateInvoiceRequest extends Request implements HasBody { @@ -21,7 +21,7 @@ class CreateInvoiceRequest extends Request implements HasBody protected Method $method = Method::POST; public function __construct( - /** @var InvoiceRequestArray request */ + /** @var CreateInvoiceRequestDtoArray request */ public readonly array $invoice, ) { } @@ -32,7 +32,7 @@ public function resolveEndpoint(): string } /** - * @return InvoiceRequestArray + * @return CreateInvoiceRequestDtoArray */ public function defaultBody(): array { diff --git a/src/Invoice/Internal/Get/GetInvoiceRequest.php b/src/Invoice/Internal/Get/GetInvoiceRequest.php new file mode 100644 index 0000000..5722bcf --- /dev/null +++ b/src/Invoice/Internal/Get/GetInvoiceRequest.php @@ -0,0 +1,49 @@ +invoiceId}"; + } + + /** + * Summary of createDtoFromResponse + * @param Response $response + * @return ApiResponse + */ + public function createDtoFromResponse(Response $response): ApiResponse + { + $code = $response->status(); + $responseArr = $response->json(); + $responseStr = $response->body(); + $responseObject = null; + + if ($response->successful()) { + InvoiceResponseValidator::for($responseArr, 'response')->validate(); + $responseObject = InvoiceSuccessfulResponse::fromArray($responseArr); + } + + return new ApiResponse( + code: $code, + response: $responseObject, + responseAsString: $responseStr + ); + } +} diff --git a/src/Invoice/Internal/Get/ListInvoiceRequest.php b/src/Invoice/Internal/Get/ListInvoiceRequest.php new file mode 100644 index 0000000..03f8ff2 --- /dev/null +++ b/src/Invoice/Internal/Get/ListInvoiceRequest.php @@ -0,0 +1,73 @@ +params->toRequestArray(); + } + + /** + * @return ApiResponse + */ + #[Override] + public function createDtoFromResponse(Response $response): ApiResponse + { + $code = $response->status(); + $responseArr = $response->json(); + $responseStr = $response->body(); + + /** + * @var InvoiceSuccessfulResponse[] + */ + $singleInvoiceResponses = []; + $metaResponse = []; + + if ($response->successful() && \array_key_exists('invoices', $responseArr) && \array_key_exists('meta', $responseArr)) { + foreach ($responseArr['invoices'] as $key => $invResponse) { + InvoiceResponseValidator::for($invResponse, "response.invoices[{$key}]")->validate(); + $singleInvoiceResponses[] = InvoiceSuccessfulResponse::fromArray($invResponse); + } + + PaginationMetadataValidator::for($responseArr['meta'], 'response.meta')->validate(); + $metaResponse = ResponsePaginator::fromArray($responseArr['meta']); + } + + $responseObj = ListInvoiceSuccessfulResponse::fromArrayOfObjects($singleInvoiceResponses, $metaResponse); + + return new ApiResponse( + code: $code, + response: $responseObj, + responseAsString: $responseStr + ); + } +} diff --git a/src/Invoice/Internal/InvoiceResource.php b/src/Invoice/Internal/InvoiceResource.php index 2886520..71ed505 100644 --- a/src/Invoice/Internal/InvoiceResource.php +++ b/src/Invoice/Internal/InvoiceResource.php @@ -3,44 +3,53 @@ namespace HamodaDev\Moyasar\Invoice\Internal; use HamodaDev\Moyasar\Invoice\APIs\CancelInvoiceRequest; -use HamodaDev\Moyasar\Invoice\APIs\GetInvoiceRequest; -use HamodaDev\Moyasar\Invoice\APIs\ListInvoicesRequest; use HamodaDev\Moyasar\Invoice\APIs\UpdateInvoiceRequest; use HamodaDev\Moyasar\Invoice\DTO\InvoiceDTO; use HamodaDev\Moyasar\Invoice\DTO\UpdateInvoiceDTO; use HamodaDev\Moyasar\Invoice\Internal\Create\BulkCreateInvoiceRequest; use HamodaDev\Moyasar\Invoice\Internal\Create\CreateInvoiceRequest; -use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\BulkInvoiceRequest; -use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\InvoiceRequest; +use HamodaDev\Moyasar\Invoice\Internal\Get\GetInvoiceRequest; +use HamodaDev\Moyasar\Invoice\Internal\Get\ListInvoiceRequest; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\BulkCreateInvoiceRequestDto; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequestDto; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\ListInvoiceRequestDto; use HamodaDev\Moyasar\Shared\DTOs\ApiResponse; use Saloon\Http\BaseResource; class InvoiceResource extends BaseResource { - public function get(string $invoiceId): InvoiceDTO + /** + * @param string $invoiceId + * @return ApiResponse<\HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoiceSuccessfulResponse> + */ + public function get(string $invoiceId): ApiResponse { return $this->connector->send(new GetInvoiceRequest($invoiceId))->dto(); } - public function list(): ListInvoicesRequest + /** + * @param ListInvoiceRequestDto $request + * @return ApiResponse<\HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\ListInvoiceSuccessfulResponse> + */ + public function list(ListInvoiceRequestDto $request): ApiResponse { - return new ListInvoicesRequest(); + return $this->connector->send(new ListInvoiceRequest($request))->dto(); } /** - * @param InvoiceRequest $invoice + * @param CreateInvoiceRequestDto $invoice * @return ApiResponse<\HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoiceSuccessfulResponse> */ - public function create(InvoiceRequest $invoice): ApiResponse + public function create(CreateInvoiceRequestDto $invoice): ApiResponse { return $this->connector->send(new CreateInvoiceRequest($invoice->toRequestArray()))->dto(); } /** - * @param BulkInvoiceRequest $invoices + * @param BulkCreateInvoiceRequestDto $invoices * @return ApiResponse<\HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\BulkInvoiceSuccessfulResponse> */ - public function bulkCreate(BulkInvoiceRequest $invoices): ApiResponse + public function bulkCreate(BulkCreateInvoiceRequestDto $invoices): ApiResponse { return $this->connector->send(new BulkCreateInvoiceRequest($invoices->toRequestArray()))->dto(); } diff --git a/src/Invoice/Shared/DTOs/Requests/BulkInvoiceRequest.php b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequestDto.php similarity index 64% rename from src/Invoice/Shared/DTOs/Requests/BulkInvoiceRequest.php rename to src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequestDto.php index 02e3c12..394253d 100644 --- a/src/Invoice/Shared/DTOs/Requests/BulkInvoiceRequest.php +++ b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequestDto.php @@ -2,31 +2,29 @@ namespace HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests; -use DateTime; -use HamodaDev\Moyasar\Shared\Const\Currency; use InvalidArgumentException; /** - * @phpstan-import-type InvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\InvoiceRequest + * @phpstan-import-type CreateInvoiceRequestDtoArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequestDto */ -final class BulkInvoiceRequest +final class BulkCreateInvoiceRequestDto { public function __construct( - /** @var InvoiceRequest[] */ + /** @var CreateInvoiceRequestDto[] */ private array $invoices ) { } /** - * @param InvoiceRequest[] $invoices - * @return BulkInvoiceRequest + * @param CreateInvoiceRequestDto[] $invoices + * @return BulkCreateInvoiceRequestDto */ public static function create( array $invoices, ): self { $result = []; foreach ($invoices as $i => $inv) { - if (!($inv instanceof InvoiceRequest)) { + if (!($inv instanceof CreateInvoiceRequestDto)) { throw new InvalidArgumentException("Invalid Invoice at index {$i}"); } $result[] = $inv; @@ -36,7 +34,7 @@ public static function create( } /** - * @return InvoiceRequestArray[] + * @return array{invoices: CreateInvoiceRequestDtoArray[]} */ public function toRequestArray(): array { @@ -49,7 +47,7 @@ public function toRequestArray(): array } /** - * @return InvoiceRequest[] + * @return CreateInvoiceRequestDto[] */ public function getInvoices(): array { diff --git a/src/Invoice/Shared/DTOs/Requests/InvoiceRequest.php b/src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequestDto.php similarity index 96% rename from src/Invoice/Shared/DTOs/Requests/InvoiceRequest.php rename to src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequestDto.php index af2eb48..ce556e6 100644 --- a/src/Invoice/Shared/DTOs/Requests/InvoiceRequest.php +++ b/src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequestDto.php @@ -7,7 +7,7 @@ use InvalidArgumentException; /** - * @phpstan-type InvoiceRequestArray array{ + * @phpstan-type CreateInvoiceRequestDtoArray array{ * amount: int, * currency: string, * description: string, @@ -18,7 +18,7 @@ * metadata?: array *} */ -final class InvoiceRequest +final class CreateInvoiceRequestDto { private const string DEFAULT_DATETIME_FORMAT = 'Y-m-d\TH:i:s.v\Z'; @@ -78,7 +78,7 @@ public static function createFromArray(array $data): self } /** - * @return InvoiceRequestArray + * @return CreateInvoiceRequestDtoArray */ public function toRequestArray(): array { diff --git a/src/Invoice/Shared/DTOs/Requests/ListInvoiceRequestDto.php b/src/Invoice/Shared/DTOs/Requests/ListInvoiceRequestDto.php new file mode 100644 index 0000000..2c805ef --- /dev/null +++ b/src/Invoice/Shared/DTOs/Requests/ListInvoiceRequestDto.php @@ -0,0 +1,53 @@ + $this->page, + ]; + + if ($this->id !== null) { + $result['id'] = $this->id; + } + if ($this->status !== null) { + $result['status'] = $this->status->value; + } + if ($this->after !== null) { + $result['created[gt]'] = $this->after->format(self::DEFAULT_DATETIME_FORMAT); + } + if ($this->before !== null) { + $result['created[lt]'] = $this->before->format(self::DEFAULT_DATETIME_FORMAT); + } + if ($this->metadataKey !== null) { + $result['metadata[key]'] = $this->metadataKey; + } + + return $result; + } + + public function nextPage(): self + { + $new = clone $this; + $new->page++; + return $new; + } +} diff --git a/src/Invoice/Shared/DTOs/Responses/ListInvoiceSuccessfulResponse.php b/src/Invoice/Shared/DTOs/Responses/ListInvoiceSuccessfulResponse.php new file mode 100644 index 0000000..af75f6e --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/ListInvoiceSuccessfulResponse.php @@ -0,0 +1,54 @@ +> $data + * @return self + */ + public static function fromArrayOfArrays(array $data, array $meta): self + { + $invoices = []; + foreach ($data as $inv) { + $invoices[] = InvoiceSuccessfulResponse::fromArray($inv); + } + + return new self($invoices, ResponsePaginator::fromArray($meta)); + } + + /** + * @return InvoiceSuccessfulResponse[] + */ + public function getInvoices(): array + { + return $this->invoices; + } + + public function getMeta(): ResponsePaginator + { + return $this->meta; + } +} diff --git a/src/Invoice/Shared/DTOs/Responses/Paginator/ResponsePaginator.php b/src/Invoice/Shared/DTOs/Responses/Paginator/ResponsePaginator.php new file mode 100644 index 0000000..0b84c33 --- /dev/null +++ b/src/Invoice/Shared/DTOs/Responses/Paginator/ResponsePaginator.php @@ -0,0 +1,51 @@ +currentPage; + } + + public function getNextPage(): int + { + return $this->nextPage; + } + + public function getPrevPage(): int + { + return $this->prevPage; + } + + public function getTotalPages(): int + { + return $this->totalPages; + } + + public function getTotalCount(): int + { + return $this->totalCount; + } +} diff --git a/tests/Config/MockResponses.php b/tests/Config/MockResponses.php index c01f83c..27fcbd9 100644 --- a/tests/Config/MockResponses.php +++ b/tests/Config/MockResponses.php @@ -2,16 +2,14 @@ namespace Tests\Config; -use HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoiceSuccessfulResponse; use Saloon\Http\Faking\MockResponse; use Tests\Config\Samples\InvoiceSamples; use HamodaDev\Moyasar\Invoice\APIs\CancelInvoiceRequest; -use HamodaDev\Moyasar\Invoice\APIs\GetInvoiceRequest; -use HamodaDev\Moyasar\Invoice\APIs\ListInvoicesRequest; use HamodaDev\Moyasar\Invoice\APIs\UpdateInvoiceRequest; use HamodaDev\Moyasar\Invoice\Internal\Create\CreateInvoiceRequest; use HamodaDev\Moyasar\Invoice\Internal\Create\BulkCreateInvoiceRequest; -use HamodaDev\Moyasar\Shared\DTOs\ApiResponse; +use HamodaDev\Moyasar\Invoice\Internal\Get\GetInvoiceRequest; +use HamodaDev\Moyasar\Invoice\Internal\Get\ListInvoiceRequest; class MockResponses { @@ -33,13 +31,19 @@ private static function invoiceResponses(): array InvoiceSamples::TEST_INVOICE_2, ], ]), + GetInvoiceRequest::class => MockResponse::make(body: InvoiceSamples::TEST_INVOICE_3), - ListInvoicesRequest::class => MockResponse::make(body: [ + ListInvoiceRequest::class => MockResponse::make(body: [ 'invoices' => [ InvoiceSamples::TEST_INVOICE, InvoiceSamples::TEST_INVOICE_2, InvoiceSamples::TEST_INVOICE_3, ], + 'meta' => [ + 'current_page' => 1, + 'total_pages' => 1, + 'total_count' => 3, + ], ]), UpdateInvoiceRequest::class => MockResponse::make(body: InvoiceSamples::TEST_INVOICE), CancelInvoiceRequest::class => MockResponse::make(body: InvoiceSamples::CANCELED_TEST_INVOICE), diff --git a/tests/Config/Samples/InvoiceSamples.php b/tests/Config/Samples/InvoiceSamples.php index a0f859f..94d0a06 100644 --- a/tests/Config/Samples/InvoiceSamples.php +++ b/tests/Config/Samples/InvoiceSamples.php @@ -62,5 +62,10 @@ class InvoiceSamples 'description' => 'test order #333', 'amount_format' => '69.00 EGP', 'url' => self::MOCK_URL, + 'created_at' => '2026-05-15 11:46:52', + 'updated_at' => '2026-05-15 11:46:52', + 'metadata' => [ + 'order_id' => '5678' + ], ]; } diff --git a/tests/Feature/Invoice/CreateInvoiceTest.php b/tests/Feature/Invoice/CreateInvoiceTest.php index bf535fe..5a6700c 100644 --- a/tests/Feature/Invoice/CreateInvoiceTest.php +++ b/tests/Feature/Invoice/CreateInvoiceTest.php @@ -1,9 +1,9 @@ getMoyasar(); // act - $invoice = $moyasar->invoice()->create(new InvoiceRequest( + $invoice = $moyasar->invoice()->create(new CreateInvoiceRequestDto( amount: 10, currency: Currency::USD, description: 'Order #1234', @@ -37,13 +37,13 @@ $moyasar = MoyasarInitializer::getInstance()->getMoyasar(); // act - $request = BulkInvoiceRequest::create([ - new InvoiceRequest( + $request = BulkCreateInvoiceRequestDto::create([ + new CreateInvoiceRequestDto( amount: 69, currency: Currency::USD, description: 'Order #1111' ), - new InvoiceRequest( + new CreateInvoiceRequestDto( amount: 70, currency: Currency::EGP, description: 'Order #2222' diff --git a/tests/Feature/Invoice/GetInvoiceTest.php b/tests/Feature/Invoice/GetInvoiceTest.php index c1ee8d7..b99ea13 100644 --- a/tests/Feature/Invoice/GetInvoiceTest.php +++ b/tests/Feature/Invoice/GetInvoiceTest.php @@ -1,6 +1,9 @@ validateEnvIsSet()); @@ -10,12 +13,12 @@ $moyasar = MoyasarInitializer::getInstance()->getMoyasar(); // act - $invoice = $moyasar->invoice()->get(getenv('MOYASAR_SAMPLE_INITIATED_INVOICE_ID') ?: '91011'); + $invoice = $moyasar->invoice()->get(getenv('MOYASAR_SAMPLE_INITIATED_INVOICE_ID') ?: '91011')->getResponse(); // assert expect($invoice) - ->toBeInstanceOf(InvoiceDTO::class) - ->and($invoice->status)->toBe('initiated'); + ->toBeInstanceOf(InvoiceSuccessfulResponse::class) + ->and($invoice->getStatus())->toBe(InvoiceStatus::INITIATED); }); it('lists invoices', function () { @@ -23,9 +26,9 @@ $moyasar = MoyasarInitializer::getInstance()->getMoyasar(); // act - $invoices = iterator_to_array($moyasar->invoice()->list()->paginate($moyasar)->items()); + $invoices = $moyasar->invoice()->list(new ListInvoiceRequestDto())->getResponse()->getInvoices(); // assert expect($invoices)->not->toBeEmpty() - ->and($invoices[0])->toBeInstanceOf(InvoiceDTO::class); + ->and($invoices[0])->toBeInstanceOf(InvoiceSuccessfulResponse::class); }); From a7c0a4da1181e4732794ecc432bf3724f08e0c66 Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sat, 23 May 2026 21:15:52 +0300 Subject: [PATCH 06/11] ref/renaming request dto classes --- ...php => BulkCreateInvoiceSaloonRequest.php} | 8 ++--- ...est.php => CreateInvoiceSaloonRequest.php} | 8 ++--- ...equest.php => GetInvoiceSaloonRequest.php} | 2 +- ...quest.php => ListInvoiceSaloonRequest.php} | 6 ++-- src/Invoice/Internal/InvoiceResource.php | 34 +++++++++---------- ...stDto.php => BulkCreateInvoiceRequest.php} | 16 ++++----- ...equestDto.php => CreateInvoiceRequest.php} | 6 ++-- ...eRequestDto.php => ListInvoiceRequest.php} | 2 +- tests/Config/MockResponses.php | 16 ++++----- tests/Feature/Invoice/CreateInvoiceTest.php | 12 +++---- tests/Feature/Invoice/GetInvoiceTest.php | 4 +-- 11 files changed, 57 insertions(+), 57 deletions(-) rename src/Invoice/Internal/Create/{BulkCreateInvoiceRequest.php => BulkCreateInvoiceSaloonRequest.php} (87%) rename src/Invoice/Internal/Create/{CreateInvoiceRequest.php => CreateInvoiceSaloonRequest.php} (84%) rename src/Invoice/Internal/Get/{GetInvoiceRequest.php => GetInvoiceSaloonRequest.php} (96%) rename src/Invoice/Internal/Get/{ListInvoiceRequest.php => ListInvoiceSaloonRequest.php} (95%) rename src/Invoice/Shared/DTOs/Requests/{BulkCreateInvoiceRequestDto.php => BulkCreateInvoiceRequest.php} (64%) rename src/Invoice/Shared/DTOs/Requests/{CreateInvoiceRequestDto.php => CreateInvoiceRequest.php} (96%) rename src/Invoice/Shared/DTOs/Requests/{ListInvoiceRequestDto.php => ListInvoiceRequest.php} (98%) diff --git a/src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php b/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php similarity index 87% rename from src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php rename to src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php index ffd3832..cc45224 100644 --- a/src/Invoice/Internal/Create/BulkCreateInvoiceRequest.php +++ b/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php @@ -13,16 +13,16 @@ use Saloon\Traits\Body\HasJsonBody; /** - * @phpstan-import-type CreateInvoiceRequestDtoArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\InvoiceRequest + * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ -class BulkCreateInvoiceRequest extends Request implements HasBody +class BulkCreateInvoiceSaloonRequest extends Request implements HasBody { use HasJsonBody; protected Method $method = Method::POST; public function __construct( - /** @var CreateInvoiceRequestDto[] request */ + /** @var CreateInvoiceRequest[] request */ public readonly array $invoices, ) { } @@ -33,7 +33,7 @@ public function resolveEndpoint(): string } /** - * @return CreateInvoiceRequestDtoArray + * @return CreateInvoiceRequestArray */ public function defaultBody(): array { diff --git a/src/Invoice/Internal/Create/CreateInvoiceRequest.php b/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php similarity index 84% rename from src/Invoice/Internal/Create/CreateInvoiceRequest.php rename to src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php index f70d34b..fe8c99c 100644 --- a/src/Invoice/Internal/Create/CreateInvoiceRequest.php +++ b/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php @@ -12,16 +12,16 @@ use Saloon\Traits\Body\HasJsonBody; /** - * @phpstan-import-type CreateInvoiceRequestDtoArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\InvoiceRequest + * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ -class CreateInvoiceRequest extends Request implements HasBody +class CreateInvoiceSaloonRequest extends Request implements HasBody { use HasJsonBody; protected Method $method = Method::POST; public function __construct( - /** @var CreateInvoiceRequestDtoArray request */ + /** @var CreateInvoiceRequestArray request */ public readonly array $invoice, ) { } @@ -32,7 +32,7 @@ public function resolveEndpoint(): string } /** - * @return CreateInvoiceRequestDtoArray + * @return CreateInvoiceRequestArray */ public function defaultBody(): array { diff --git a/src/Invoice/Internal/Get/GetInvoiceRequest.php b/src/Invoice/Internal/Get/GetInvoiceSaloonRequest.php similarity index 96% rename from src/Invoice/Internal/Get/GetInvoiceRequest.php rename to src/Invoice/Internal/Get/GetInvoiceSaloonRequest.php index 5722bcf..088eb87 100644 --- a/src/Invoice/Internal/Get/GetInvoiceRequest.php +++ b/src/Invoice/Internal/Get/GetInvoiceSaloonRequest.php @@ -9,7 +9,7 @@ use Saloon\Http\Request; use Saloon\Http\Response; -class GetInvoiceRequest extends Request +class GetInvoiceSaloonRequest extends Request { protected Method $method = Method::GET; diff --git a/src/Invoice/Internal/Get/ListInvoiceRequest.php b/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php similarity index 95% rename from src/Invoice/Internal/Get/ListInvoiceRequest.php rename to src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php index 03f8ff2..60a8b3e 100644 --- a/src/Invoice/Internal/Get/ListInvoiceRequest.php +++ b/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php @@ -5,7 +5,7 @@ use HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoiceSuccessfulResponse; use HamodaDev\Moyasar\Invoice\Internal\Core\Validators\InvoiceResponseValidator; use HamodaDev\Moyasar\Invoice\Internal\Core\Validators\Pagination\PaginationMetadataValidator; -use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\ListInvoiceRequestDto; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\ListInvoiceRequest; use HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\ListInvoiceSuccessfulResponse; use HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\Paginator\ResponsePaginator; use HamodaDev\Moyasar\Shared\DTOs\ApiResponse; @@ -14,12 +14,12 @@ use Saloon\Http\Request; use Saloon\Http\Response; -class ListInvoiceRequest extends Request +class ListInvoiceSaloonRequest extends Request { protected Method $method = Method::GET; public function __construct( - protected readonly ListInvoiceRequestDto $params + protected readonly ListInvoiceRequest $params ) { } diff --git a/src/Invoice/Internal/InvoiceResource.php b/src/Invoice/Internal/InvoiceResource.php index 71ed505..8843146 100644 --- a/src/Invoice/Internal/InvoiceResource.php +++ b/src/Invoice/Internal/InvoiceResource.php @@ -6,13 +6,13 @@ use HamodaDev\Moyasar\Invoice\APIs\UpdateInvoiceRequest; use HamodaDev\Moyasar\Invoice\DTO\InvoiceDTO; use HamodaDev\Moyasar\Invoice\DTO\UpdateInvoiceDTO; -use HamodaDev\Moyasar\Invoice\Internal\Create\BulkCreateInvoiceRequest; -use HamodaDev\Moyasar\Invoice\Internal\Create\CreateInvoiceRequest; -use HamodaDev\Moyasar\Invoice\Internal\Get\GetInvoiceRequest; -use HamodaDev\Moyasar\Invoice\Internal\Get\ListInvoiceRequest; -use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\BulkCreateInvoiceRequestDto; -use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequestDto; -use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\ListInvoiceRequestDto; +use HamodaDev\Moyasar\Invoice\Internal\Create\BulkCreateInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Internal\Create\CreateInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Internal\Get\GetInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Internal\Get\ListInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\BulkCreateInvoiceRequest; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\ListInvoiceRequest; use HamodaDev\Moyasar\Shared\DTOs\ApiResponse; use Saloon\Http\BaseResource; @@ -24,34 +24,34 @@ class InvoiceResource extends BaseResource */ public function get(string $invoiceId): ApiResponse { - return $this->connector->send(new GetInvoiceRequest($invoiceId))->dto(); + return $this->connector->send(new GetInvoiceSaloonRequest($invoiceId))->dto(); } /** - * @param ListInvoiceRequestDto $request + * @param ListInvoiceRequest $request * @return ApiResponse<\HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\ListInvoiceSuccessfulResponse> */ - public function list(ListInvoiceRequestDto $request): ApiResponse + public function list(ListInvoiceRequest $request): ApiResponse { - return $this->connector->send(new ListInvoiceRequest($request))->dto(); + return $this->connector->send(new ListInvoiceSaloonRequest($request))->dto(); } /** - * @param CreateInvoiceRequestDto $invoice + * @param CreateInvoiceRequest $invoice * @return ApiResponse<\HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoiceSuccessfulResponse> */ - public function create(CreateInvoiceRequestDto $invoice): ApiResponse + public function create(CreateInvoiceRequest $invoice): ApiResponse { - return $this->connector->send(new CreateInvoiceRequest($invoice->toRequestArray()))->dto(); + return $this->connector->send(new CreateInvoiceSaloonRequest($invoice->toRequestArray()))->dto(); } /** - * @param BulkCreateInvoiceRequestDto $invoices + * @param BulkCreateInvoiceRequest $invoices * @return ApiResponse<\HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\BulkInvoiceSuccessfulResponse> */ - public function bulkCreate(BulkCreateInvoiceRequestDto $invoices): ApiResponse + public function bulkCreate(BulkCreateInvoiceRequest $invoices): ApiResponse { - return $this->connector->send(new BulkCreateInvoiceRequest($invoices->toRequestArray()))->dto(); + return $this->connector->send(new BulkCreateInvoiceSaloonRequest($invoices->toRequestArray()))->dto(); } public function update(string $invoiceId, UpdateInvoiceDTO $dto): InvoiceDTO diff --git a/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequestDto.php b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php similarity index 64% rename from src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequestDto.php rename to src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php index 394253d..316cb8f 100644 --- a/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequestDto.php +++ b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php @@ -5,26 +5,26 @@ use InvalidArgumentException; /** - * @phpstan-import-type CreateInvoiceRequestDtoArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequestDto + * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ -final class BulkCreateInvoiceRequestDto +final class BulkCreateInvoiceRequest { public function __construct( - /** @var CreateInvoiceRequestDto[] */ + /** @var CreateInvoiceRequest[] */ private array $invoices ) { } /** - * @param CreateInvoiceRequestDto[] $invoices - * @return BulkCreateInvoiceRequestDto + * @param CreateInvoiceRequest[] $invoices + * @return BulkCreateInvoiceRequest */ public static function create( array $invoices, ): self { $result = []; foreach ($invoices as $i => $inv) { - if (!($inv instanceof CreateInvoiceRequestDto)) { + if (!($inv instanceof CreateInvoiceRequest)) { throw new InvalidArgumentException("Invalid Invoice at index {$i}"); } $result[] = $inv; @@ -34,7 +34,7 @@ public static function create( } /** - * @return array{invoices: CreateInvoiceRequestDtoArray[]} + * @return array{invoices: CreateInvoiceRequestArray[]} */ public function toRequestArray(): array { @@ -47,7 +47,7 @@ public function toRequestArray(): array } /** - * @return CreateInvoiceRequestDto[] + * @return CreateInvoiceRequest[] */ public function getInvoices(): array { diff --git a/src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequestDto.php b/src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequest.php similarity index 96% rename from src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequestDto.php rename to src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequest.php index ce556e6..f61d905 100644 --- a/src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequestDto.php +++ b/src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequest.php @@ -7,7 +7,7 @@ use InvalidArgumentException; /** - * @phpstan-type CreateInvoiceRequestDtoArray array{ + * @phpstan-type CreateInvoiceRequestArray array{ * amount: int, * currency: string, * description: string, @@ -18,7 +18,7 @@ * metadata?: array *} */ -final class CreateInvoiceRequestDto +final class CreateInvoiceRequest { private const string DEFAULT_DATETIME_FORMAT = 'Y-m-d\TH:i:s.v\Z'; @@ -78,7 +78,7 @@ public static function createFromArray(array $data): self } /** - * @return CreateInvoiceRequestDtoArray + * @return CreateInvoiceRequestArray */ public function toRequestArray(): array { diff --git a/src/Invoice/Shared/DTOs/Requests/ListInvoiceRequestDto.php b/src/Invoice/Shared/DTOs/Requests/ListInvoiceRequest.php similarity index 98% rename from src/Invoice/Shared/DTOs/Requests/ListInvoiceRequestDto.php rename to src/Invoice/Shared/DTOs/Requests/ListInvoiceRequest.php index 2c805ef..aefcffa 100644 --- a/src/Invoice/Shared/DTOs/Requests/ListInvoiceRequestDto.php +++ b/src/Invoice/Shared/DTOs/Requests/ListInvoiceRequest.php @@ -5,7 +5,7 @@ use DateTimeImmutable; use HamodaDev\Moyasar\Invoice\Internal\Core\Const\InvoiceStatus; -class ListInvoiceRequestDto +class ListInvoiceRequest { private const string DEFAULT_DATETIME_FORMAT = 'Y-m-d\TH:i:s.v\Z'; diff --git a/tests/Config/MockResponses.php b/tests/Config/MockResponses.php index 27fcbd9..d5708e2 100644 --- a/tests/Config/MockResponses.php +++ b/tests/Config/MockResponses.php @@ -6,10 +6,10 @@ use Tests\Config\Samples\InvoiceSamples; use HamodaDev\Moyasar\Invoice\APIs\CancelInvoiceRequest; use HamodaDev\Moyasar\Invoice\APIs\UpdateInvoiceRequest; -use HamodaDev\Moyasar\Invoice\Internal\Create\CreateInvoiceRequest; -use HamodaDev\Moyasar\Invoice\Internal\Create\BulkCreateInvoiceRequest; -use HamodaDev\Moyasar\Invoice\Internal\Get\GetInvoiceRequest; -use HamodaDev\Moyasar\Invoice\Internal\Get\ListInvoiceRequest; +use HamodaDev\Moyasar\Invoice\Internal\Create\CreateInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Internal\Create\BulkCreateInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Internal\Get\GetInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Internal\Get\ListInvoiceSaloonRequest; class MockResponses { @@ -24,16 +24,16 @@ public static function getAll(): array private static function invoiceResponses(): array { return [ - CreateInvoiceRequest::class => MockResponse::make(body: InvoiceSamples::TEST_INVOICE), - BulkCreateInvoiceRequest::class => MockResponse::make(body: [ + CreateInvoiceSaloonRequest::class => MockResponse::make(body: InvoiceSamples::TEST_INVOICE), + BulkCreateInvoiceSaloonRequest::class => MockResponse::make(body: [ 'invoices' => [ InvoiceSamples::TEST_INVOICE, InvoiceSamples::TEST_INVOICE_2, ], ]), - GetInvoiceRequest::class => MockResponse::make(body: InvoiceSamples::TEST_INVOICE_3), - ListInvoiceRequest::class => MockResponse::make(body: [ + GetInvoiceSaloonRequest::class => MockResponse::make(body: InvoiceSamples::TEST_INVOICE_3), + ListInvoiceSaloonRequest::class => MockResponse::make(body: [ 'invoices' => [ InvoiceSamples::TEST_INVOICE, InvoiceSamples::TEST_INVOICE_2, diff --git a/tests/Feature/Invoice/CreateInvoiceTest.php b/tests/Feature/Invoice/CreateInvoiceTest.php index 5a6700c..9bf42ad 100644 --- a/tests/Feature/Invoice/CreateInvoiceTest.php +++ b/tests/Feature/Invoice/CreateInvoiceTest.php @@ -1,9 +1,9 @@ getMoyasar(); // act - $invoice = $moyasar->invoice()->create(new CreateInvoiceRequestDto( + $invoice = $moyasar->invoice()->create(new CreateInvoiceRequest( amount: 10, currency: Currency::USD, description: 'Order #1234', @@ -37,13 +37,13 @@ $moyasar = MoyasarInitializer::getInstance()->getMoyasar(); // act - $request = BulkCreateInvoiceRequestDto::create([ - new CreateInvoiceRequestDto( + $request = BulkCreateInvoiceRequest::create([ + new CreateInvoiceRequest( amount: 69, currency: Currency::USD, description: 'Order #1111' ), - new CreateInvoiceRequestDto( + new CreateInvoiceRequest( amount: 70, currency: Currency::EGP, description: 'Order #2222' diff --git a/tests/Feature/Invoice/GetInvoiceTest.php b/tests/Feature/Invoice/GetInvoiceTest.php index b99ea13..2eeed6f 100644 --- a/tests/Feature/Invoice/GetInvoiceTest.php +++ b/tests/Feature/Invoice/GetInvoiceTest.php @@ -2,7 +2,7 @@ use HamodaDev\Moyasar\Invoice\DTO\InvoiceDTO; use HamodaDev\Moyasar\Invoice\Internal\Core\Const\InvoiceStatus; -use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\ListInvoiceRequestDto; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\ListInvoiceRequest; use HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoiceSuccessfulResponse; use Tests\Config\MoyasarInitializer; @@ -26,7 +26,7 @@ $moyasar = MoyasarInitializer::getInstance()->getMoyasar(); // act - $invoices = $moyasar->invoice()->list(new ListInvoiceRequestDto())->getResponse()->getInvoices(); + $invoices = $moyasar->invoice()->list(new ListInvoiceRequest())->getResponse()->getInvoices(); // assert expect($invoices)->not->toBeEmpty() From 2217d3785b378c2fc8dd76204ee364c8002708c4 Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sat, 23 May 2026 23:12:00 +0300 Subject: [PATCH 07/11] feat:invoice module done, with all tests, cs checks, and static analysis --- src/Invoice/APIs/CancelInvoiceRequest.php | 28 -------- src/Invoice/APIs/UpdateInvoiceRequest.php | 41 ----------- src/Invoice/DTO/CreateInvoiceDTO.php | 32 --------- src/Invoice/DTO/InvoiceDTO.php | 69 ------------------- src/Invoice/DTO/UpdateInvoiceDTO.php | 14 ---- .../Cancel/CancelInvoiceSaloonRequest.php | 52 ++++++++++++++ .../Create/BulkCreateInvoiceSaloonRequest.php | 6 +- .../Internal/Get/ListInvoiceSaloonRequest.php | 1 - src/Invoice/Internal/InvoiceResource.php | 23 ++++--- .../Update/UpdateInvoiceSaloonRequest.php | 64 +++++++++++++++++ .../Requests/BulkCreateInvoiceRequest.php | 2 +- .../DTOs/Requests/UpdateInvoiceRequest.php | 47 +++++++++++++ tests/Config/MockResponses.php | 8 +-- tests/Config/Samples/InvoiceSamples.php | 8 ++- tests/Feature/Invoice/CancelInvoiceTest.php | 20 ++++-- tests/Feature/Invoice/GetInvoiceTest.php | 1 - tests/Feature/Invoice/UpdateInvoiceTest.php | 26 ++++--- 17 files changed, 220 insertions(+), 222 deletions(-) delete mode 100644 src/Invoice/APIs/CancelInvoiceRequest.php delete mode 100644 src/Invoice/APIs/UpdateInvoiceRequest.php delete mode 100644 src/Invoice/DTO/CreateInvoiceDTO.php delete mode 100644 src/Invoice/DTO/InvoiceDTO.php delete mode 100644 src/Invoice/DTO/UpdateInvoiceDTO.php create mode 100644 src/Invoice/Internal/Cancel/CancelInvoiceSaloonRequest.php create mode 100644 src/Invoice/Internal/Update/UpdateInvoiceSaloonRequest.php create mode 100644 src/Invoice/Shared/DTOs/Requests/UpdateInvoiceRequest.php diff --git a/src/Invoice/APIs/CancelInvoiceRequest.php b/src/Invoice/APIs/CancelInvoiceRequest.php deleted file mode 100644 index 12ae829..0000000 --- a/src/Invoice/APIs/CancelInvoiceRequest.php +++ /dev/null @@ -1,28 +0,0 @@ -invoiceId}/cancel"; - } - - public function createDtoFromResponse(Response $response): InvoiceDTO - { - return InvoiceDTO::fromResponse($response); - } -} diff --git a/src/Invoice/APIs/UpdateInvoiceRequest.php b/src/Invoice/APIs/UpdateInvoiceRequest.php deleted file mode 100644 index d06f8a6..0000000 --- a/src/Invoice/APIs/UpdateInvoiceRequest.php +++ /dev/null @@ -1,41 +0,0 @@ -invoiceId}"; - } - - public function defaultBody(): array - { - return [ - 'metadata' => $this->updateInvoiceDTO->metadata, - ]; - } - - public function createDtoFromResponse(Response $response): InvoiceDTO - { - return InvoiceDTO::fromResponse($response); - } -} diff --git a/src/Invoice/DTO/CreateInvoiceDTO.php b/src/Invoice/DTO/CreateInvoiceDTO.php deleted file mode 100644 index 4555f00..0000000 --- a/src/Invoice/DTO/CreateInvoiceDTO.php +++ /dev/null @@ -1,32 +0,0 @@ - $metadata - * @param array $payments - * Note: $logoUrl is not a part of the ListInvoice response, so, it's made nullable - */ - public function __construct( - public string $id, - public string $status, - public int $amount, - public string $currency, - public string $description, - public ?string $logoUrl, - public string $amountFormat, - public string $url, - public ?string $callbackUrl = null, - public ?string $expiredAt = null, - public string $createdAt = '', - public string $updatedAt = '', - public ?string $backUrl = null, - public ?string $successUrl = null, - public array $metadata = [], - public array $payments = [], - ) { - } - - public static function fromResponse(Response $response): self - { - return self::fromArray($response->json()); - } - - /** - * @param array $data - */ - public static function fromArray(array $data): self - { - return new self( - id: $data['id'], - status: $data['status'], - amount: (int) $data['amount'], - currency: $data['currency'], - description: $data['description'], - logoUrl: $data['logo_url'] ?? null, - amountFormat: $data['amount_format'], - url: $data['url'], - callbackUrl: $data['callback_url'] ?? null, - expiredAt: $data['expired_at'] ?? null, - createdAt: $data['created_at'] ?? '', - updatedAt: $data['updated_at'] ?? '', - backUrl: $data['back_url'] ?? null, - successUrl: $data['success_url'] ?? null, - metadata: $data['metadata'] ?? [], - payments: $data['payments'] ?? [], - ); - } -} diff --git a/src/Invoice/DTO/UpdateInvoiceDTO.php b/src/Invoice/DTO/UpdateInvoiceDTO.php deleted file mode 100644 index 1c161ba..0000000 --- a/src/Invoice/DTO/UpdateInvoiceDTO.php +++ /dev/null @@ -1,14 +0,0 @@ - $metadata - */ - public function __construct( - public array $metadata, - ) { - } -} diff --git a/src/Invoice/Internal/Cancel/CancelInvoiceSaloonRequest.php b/src/Invoice/Internal/Cancel/CancelInvoiceSaloonRequest.php new file mode 100644 index 0000000..0e68ac8 --- /dev/null +++ b/src/Invoice/Internal/Cancel/CancelInvoiceSaloonRequest.php @@ -0,0 +1,52 @@ +invoiceId}/cancel"; + } + + /** + * Summary of createDtoFromResponse + * @param Response $response + * @return ApiResponse + */ + #[Override] + public function createDtoFromResponse(Response $response): ApiResponse + { + $code = $response->status(); + $responseArr = $response->json(); + $responseStr = $response->body(); + $responseObject = null; + + if ($response->successful()) { + InvoiceResponseValidator::for($responseArr, 'response')->validate(); + $responseObject = InvoiceSuccessfulResponse::fromArray($responseArr); + } + + return new ApiResponse( + code: $code, + response: $responseObject, + responseAsString: $responseStr + ); + } +} diff --git a/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php b/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php index cc45224..81e829c 100644 --- a/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php @@ -22,7 +22,7 @@ class BulkCreateInvoiceSaloonRequest extends Request implements HasBody protected Method $method = Method::POST; public function __construct( - /** @var CreateInvoiceRequest[] request */ + /** @var array{invoices: CreateInvoiceRequestArray[]} request */ public readonly array $invoices, ) { } @@ -33,11 +33,11 @@ public function resolveEndpoint(): string } /** - * @return CreateInvoiceRequestArray + * @return array{invoices: CreateInvoiceRequestArray[]} */ public function defaultBody(): array { - return array_filter($this->invoices, fn(mixed $value): bool => $value !== null); + return $this->invoices; } /** diff --git a/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php b/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php index 60a8b3e..85d21d7 100644 --- a/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php @@ -27,7 +27,6 @@ public function __construct( public function resolveEndpoint(): string { return "/invoices"; - } #[Override] diff --git a/src/Invoice/Internal/InvoiceResource.php b/src/Invoice/Internal/InvoiceResource.php index 8843146..05fe2ca 100644 --- a/src/Invoice/Internal/InvoiceResource.php +++ b/src/Invoice/Internal/InvoiceResource.php @@ -2,17 +2,16 @@ namespace HamodaDev\Moyasar\Invoice\Internal; -use HamodaDev\Moyasar\Invoice\APIs\CancelInvoiceRequest; -use HamodaDev\Moyasar\Invoice\APIs\UpdateInvoiceRequest; -use HamodaDev\Moyasar\Invoice\DTO\InvoiceDTO; -use HamodaDev\Moyasar\Invoice\DTO\UpdateInvoiceDTO; use HamodaDev\Moyasar\Invoice\Internal\Create\BulkCreateInvoiceSaloonRequest; use HamodaDev\Moyasar\Invoice\Internal\Create\CreateInvoiceSaloonRequest; use HamodaDev\Moyasar\Invoice\Internal\Get\GetInvoiceSaloonRequest; use HamodaDev\Moyasar\Invoice\Internal\Get\ListInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Internal\Cancel\CancelInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Internal\Update\UpdateInvoiceSaloonRequest; use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\BulkCreateInvoiceRequest; use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest; use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\ListInvoiceRequest; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\UpdateInvoiceRequest; use HamodaDev\Moyasar\Shared\DTOs\ApiResponse; use Saloon\Http\BaseResource; @@ -54,13 +53,21 @@ public function bulkCreate(BulkCreateInvoiceRequest $invoices): ApiResponse return $this->connector->send(new BulkCreateInvoiceSaloonRequest($invoices->toRequestArray()))->dto(); } - public function update(string $invoiceId, UpdateInvoiceDTO $dto): InvoiceDTO + /** + * @param UpdateInvoiceRequest $request + * @return ApiResponse<\HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoiceSuccessfulResponse> + */ + public function update(UpdateInvoiceRequest $request): ApiResponse { - return $this->connector->send(new UpdateInvoiceRequest($invoiceId, $dto))->dto(); + return $this->connector->send(new UpdateInvoiceSaloonRequest($request->getInvoiceId(), $request->toRequestBodyArray()))->dto(); } - public function cancel(string $invoiceId): InvoiceDTO + /** + * @param string $invoiceId + * @return ApiResponse<\HamodaDev\Moyasar\Invoice\Shared\DTOs\Responses\InvoiceSuccessfulResponse> + */ + public function cancel(string $invoiceId): ApiResponse { - return $this->connector->send(new CancelInvoiceRequest($invoiceId))->dto(); + return $this->connector->send(new CancelInvoiceSaloonRequest($invoiceId))->dto(); } } diff --git a/src/Invoice/Internal/Update/UpdateInvoiceSaloonRequest.php b/src/Invoice/Internal/Update/UpdateInvoiceSaloonRequest.php new file mode 100644 index 0000000..5435f44 --- /dev/null +++ b/src/Invoice/Internal/Update/UpdateInvoiceSaloonRequest.php @@ -0,0 +1,64 @@ +} */ + private readonly array $metadata, + ) { + } + + #[Override] + public function resolveEndpoint(): string + { + return "/invoices/{$this->invoiceId}"; + } + + protected function defaultBody(): array + { + return $this->metadata; + } + + /** + * Summary of createDtoFromResponse + * @param Response $response + * @return ApiResponse + */ + #[Override] + public function createDtoFromResponse(Response $response): ApiResponse + { + $code = $response->status(); + $responseArr = $response->json(); + $responseStr = $response->body(); + $responseObject = null; + + if ($response->successful()) { + InvoiceResponseValidator::for($responseArr, 'response')->validate(); + $responseObject = InvoiceSuccessfulResponse::fromArray($responseArr); + } + + return new ApiResponse( + code: $code, + response: $responseObject, + responseAsString: $responseStr + ); + } +} diff --git a/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php index 316cb8f..755256b 100644 --- a/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php +++ b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php @@ -16,7 +16,7 @@ public function __construct( } /** - * @param CreateInvoiceRequest[] $invoices + * @param mixed[] $invoices * @return BulkCreateInvoiceRequest */ public static function create( diff --git a/src/Invoice/Shared/DTOs/Requests/UpdateInvoiceRequest.php b/src/Invoice/Shared/DTOs/Requests/UpdateInvoiceRequest.php new file mode 100644 index 0000000..c2d9a3b --- /dev/null +++ b/src/Invoice/Shared/DTOs/Requests/UpdateInvoiceRequest.php @@ -0,0 +1,47 @@ + */ + private array $metadata + ) { + } + + /** + * renamed this method to `of` instead of `create` to reduce confison `UpdateInvoiceRequest::create(...)` ❌ + * But UpdateInvoiceRequest::create(...)` is more readable + * @param string $invoiceId + * @param array $metadata + * @return UpdateInvoiceRequest + */ + public static function of(string $invoiceId, array $metadata): self + { + return new self($invoiceId, $metadata); + } + + /** + * returns the request body as an associative array + * Note: it doesn't return the invoice id as a key + * @return array{metadata: array} the request body + */ + public function toRequestBodyArray(): array + { + return [ + 'metadata' => $this->metadata + ]; + } + + public function getInvoiceId(): string + { + return $this->invoiceId; + } + + public function getMetadata(): array + { + return $this->metadata; + } +} diff --git a/tests/Config/MockResponses.php b/tests/Config/MockResponses.php index d5708e2..6915014 100644 --- a/tests/Config/MockResponses.php +++ b/tests/Config/MockResponses.php @@ -4,12 +4,12 @@ use Saloon\Http\Faking\MockResponse; use Tests\Config\Samples\InvoiceSamples; -use HamodaDev\Moyasar\Invoice\APIs\CancelInvoiceRequest; -use HamodaDev\Moyasar\Invoice\APIs\UpdateInvoiceRequest; use HamodaDev\Moyasar\Invoice\Internal\Create\CreateInvoiceSaloonRequest; use HamodaDev\Moyasar\Invoice\Internal\Create\BulkCreateInvoiceSaloonRequest; use HamodaDev\Moyasar\Invoice\Internal\Get\GetInvoiceSaloonRequest; use HamodaDev\Moyasar\Invoice\Internal\Get\ListInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Internal\Cancel\CancelInvoiceSaloonRequest; +use HamodaDev\Moyasar\Invoice\Internal\Update\UpdateInvoiceSaloonRequest; class MockResponses { @@ -45,8 +45,8 @@ private static function invoiceResponses(): array 'total_count' => 3, ], ]), - UpdateInvoiceRequest::class => MockResponse::make(body: InvoiceSamples::TEST_INVOICE), - CancelInvoiceRequest::class => MockResponse::make(body: InvoiceSamples::CANCELED_TEST_INVOICE), + UpdateInvoiceSaloonRequest::class => MockResponse::make(body: InvoiceSamples::TEST_INVOICE), + CancelInvoiceSaloonRequest::class => MockResponse::make(body: InvoiceSamples::CANCELED_TEST_INVOICE), ]; } } diff --git a/tests/Config/Samples/InvoiceSamples.php b/tests/Config/Samples/InvoiceSamples.php index 94d0a06..f74809a 100644 --- a/tests/Config/Samples/InvoiceSamples.php +++ b/tests/Config/Samples/InvoiceSamples.php @@ -16,8 +16,8 @@ class InvoiceSamples 'description' => 'test order', 'amount_format' => '10.00 USD', 'url' => self::MOCK_URL, - 'created_at' => '2026-05-12 11:46:52', - 'updated_at' => '2026-05-12 11:46:52', + 'created_at' => '2026-05-14 11:46:52', + 'updated_at' => '2026-05-14 11:46:52', 'metadata' => [ 'order_id' => '1234' ], @@ -32,6 +32,8 @@ class InvoiceSamples 'description' => 'test order', 'amount_format' => '10.00 USD', 'url' => self::MOCK_URL, + 'created_at' => '2026-05-12 11:46:52', + 'updated_at' => '2026-05-12 11:46:52', 'metadata' => [ 'order_id' => '1234' ], @@ -65,7 +67,7 @@ class InvoiceSamples 'created_at' => '2026-05-15 11:46:52', 'updated_at' => '2026-05-15 11:46:52', 'metadata' => [ - 'order_id' => '5678' + 'order_id' => '91011' ], ]; } diff --git a/tests/Feature/Invoice/CancelInvoiceTest.php b/tests/Feature/Invoice/CancelInvoiceTest.php index d164ef1..89215ef 100644 --- a/tests/Feature/Invoice/CancelInvoiceTest.php +++ b/tests/Feature/Invoice/CancelInvoiceTest.php @@ -1,7 +1,9 @@ validateEnvIsSet()); @@ -10,13 +12,17 @@ // arrange $moyasar = MoyasarInitializer::getInstance()->getMoyasar(); - $invoice = $moyasar->invoice()->create(CreateInvoiceDTO::fromArray(mockCreateInvoiceDTO())); + $invoice = $moyasar->invoice()->create(new CreateInvoiceRequest( + amount: 80, + currency: Currency::SAR, + description: 'Order #1234' + ))->getResponse(); // act - $cancelled = $moyasar->invoice()->cancel($invoice->id); + $cancelled = $moyasar->invoice()->cancel($invoice->getId())->getResponse(); // assert - expect($cancelled)->toBeInstanceOf(InvoiceDTO::class) - ->and($cancelled->id)->toBe($invoice->id) - ->and($cancelled->status)->toBe('canceled'); + expect($cancelled)->toBeInstanceOf(InvoiceSuccessfulResponse::class) + ->and($cancelled->getId())->toBe($invoice->getId()) + ->and($cancelled->getStatus())->toBe(InvoiceStatus::CANCELED); }); diff --git a/tests/Feature/Invoice/GetInvoiceTest.php b/tests/Feature/Invoice/GetInvoiceTest.php index 2eeed6f..3804864 100644 --- a/tests/Feature/Invoice/GetInvoiceTest.php +++ b/tests/Feature/Invoice/GetInvoiceTest.php @@ -1,6 +1,5 @@ validateEnvIsSet()); @@ -11,15 +12,20 @@ // arrange $moyasar = MoyasarInitializer::getInstance()->getMoyasar(); - $invoice = $moyasar->invoice()->create(CreateInvoiceDTO::fromArray(mockCreateInvoiceDTO())); + $invoice = $moyasar->invoice()->create(new CreateInvoiceRequest( + amount: 10, + currency: Currency::USD, + description: 'Order #1234', + callbackUrl: 'https://example.com/webhooks/moyasar', + ))->getResponse(); // act - $updated = $moyasar->invoice()->update($invoice->id, new UpdateInvoiceDTO( - metadata: ['order_id' => '1234'], - )); + $updated = $moyasar->invoice() + ->update(UpdateInvoiceRequest::of($invoice->getId(), ['order_id' => '1234'])) + ->getResponse(); // assert - expect($updated)->toBeInstanceOf(InvoiceDTO::class) - ->and($updated->id)->toBe($invoice->id) - ->and($updated->metadata)->toBe(['order_id' => '1234']); + expect($updated)->toBeInstanceOf(InvoiceSuccessfulResponse::class) + ->and($updated->getId())->toBe($invoice->getId()) + ->and($updated->getMetadata())->toBe(['order_id' => '1234']); }); From f845ee557f373de48a9170d3d97a3d8d3a4afb0c Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sat, 23 May 2026 23:20:23 +0300 Subject: [PATCH 08/11] fix: minor cs warning fixes --- .../Internal/Create/BulkCreateInvoiceSaloonRequest.php | 3 ++- src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php | 3 ++- src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php | 6 +++++- src/Invoice/Internal/InvoiceResource.php | 4 +++- .../Shared/DTOs/Requests/BulkCreateInvoiceRequest.php | 3 ++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php b/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php index 81e829c..3e9ba66 100644 --- a/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php @@ -13,7 +13,8 @@ use Saloon\Traits\Body\HasJsonBody; /** - * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest + * @phpstan-import-type CreateInvoiceRequestArray + * from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ class BulkCreateInvoiceSaloonRequest extends Request implements HasBody { diff --git a/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php b/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php index fe8c99c..7540a62 100644 --- a/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php @@ -12,7 +12,8 @@ use Saloon\Traits\Body\HasJsonBody; /** - * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest + * @phpstan-import-type CreateInvoiceRequestArray + * from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ class CreateInvoiceSaloonRequest extends Request implements HasBody { diff --git a/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php b/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php index 85d21d7..a137935 100644 --- a/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php @@ -51,7 +51,11 @@ public function createDtoFromResponse(Response $response): ApiResponse $singleInvoiceResponses = []; $metaResponse = []; - if ($response->successful() && \array_key_exists('invoices', $responseArr) && \array_key_exists('meta', $responseArr)) { + if ( + $response->successful() + && \array_key_exists('invoices', $responseArr) + && \array_key_exists('meta', $responseArr) + ) { foreach ($responseArr['invoices'] as $key => $invResponse) { InvoiceResponseValidator::for($invResponse, "response.invoices[{$key}]")->validate(); $singleInvoiceResponses[] = InvoiceSuccessfulResponse::fromArray($invResponse); diff --git a/src/Invoice/Internal/InvoiceResource.php b/src/Invoice/Internal/InvoiceResource.php index 05fe2ca..dc6678b 100644 --- a/src/Invoice/Internal/InvoiceResource.php +++ b/src/Invoice/Internal/InvoiceResource.php @@ -59,7 +59,9 @@ public function bulkCreate(BulkCreateInvoiceRequest $invoices): ApiResponse */ public function update(UpdateInvoiceRequest $request): ApiResponse { - return $this->connector->send(new UpdateInvoiceSaloonRequest($request->getInvoiceId(), $request->toRequestBodyArray()))->dto(); + return $this->connector->send( + new UpdateInvoiceSaloonRequest($request->getInvoiceId(), $request->toRequestBodyArray()) + )->dto(); } /** diff --git a/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php index 755256b..73d95e0 100644 --- a/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php +++ b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php @@ -5,7 +5,8 @@ use InvalidArgumentException; /** - * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest + * @phpstan-import-type CreateInvoiceRequestArray + * from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ final class BulkCreateInvoiceRequest { From df9d957c0e3d254949945595542203748c4bb890 Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sat, 23 May 2026 23:23:16 +0300 Subject: [PATCH 09/11] Revert "fix: minor cs warning fixes" This reverts commit f845ee557f373de48a9170d3d97a3d8d3a4afb0c. --- .../Internal/Create/BulkCreateInvoiceSaloonRequest.php | 3 +-- src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php | 3 +-- src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php | 6 +----- src/Invoice/Internal/InvoiceResource.php | 4 +--- .../Shared/DTOs/Requests/BulkCreateInvoiceRequest.php | 3 +-- 5 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php b/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php index 3e9ba66..81e829c 100644 --- a/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php @@ -13,8 +13,7 @@ use Saloon\Traits\Body\HasJsonBody; /** - * @phpstan-import-type CreateInvoiceRequestArray - * from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest + * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ class BulkCreateInvoiceSaloonRequest extends Request implements HasBody { diff --git a/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php b/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php index 7540a62..fe8c99c 100644 --- a/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php @@ -12,8 +12,7 @@ use Saloon\Traits\Body\HasJsonBody; /** - * @phpstan-import-type CreateInvoiceRequestArray - * from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest + * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ class CreateInvoiceSaloonRequest extends Request implements HasBody { diff --git a/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php b/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php index a137935..85d21d7 100644 --- a/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php @@ -51,11 +51,7 @@ public function createDtoFromResponse(Response $response): ApiResponse $singleInvoiceResponses = []; $metaResponse = []; - if ( - $response->successful() - && \array_key_exists('invoices', $responseArr) - && \array_key_exists('meta', $responseArr) - ) { + if ($response->successful() && \array_key_exists('invoices', $responseArr) && \array_key_exists('meta', $responseArr)) { foreach ($responseArr['invoices'] as $key => $invResponse) { InvoiceResponseValidator::for($invResponse, "response.invoices[{$key}]")->validate(); $singleInvoiceResponses[] = InvoiceSuccessfulResponse::fromArray($invResponse); diff --git a/src/Invoice/Internal/InvoiceResource.php b/src/Invoice/Internal/InvoiceResource.php index dc6678b..05fe2ca 100644 --- a/src/Invoice/Internal/InvoiceResource.php +++ b/src/Invoice/Internal/InvoiceResource.php @@ -59,9 +59,7 @@ public function bulkCreate(BulkCreateInvoiceRequest $invoices): ApiResponse */ public function update(UpdateInvoiceRequest $request): ApiResponse { - return $this->connector->send( - new UpdateInvoiceSaloonRequest($request->getInvoiceId(), $request->toRequestBodyArray()) - )->dto(); + return $this->connector->send(new UpdateInvoiceSaloonRequest($request->getInvoiceId(), $request->toRequestBodyArray()))->dto(); } /** diff --git a/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php index 73d95e0..755256b 100644 --- a/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php +++ b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php @@ -5,8 +5,7 @@ use InvalidArgumentException; /** - * @phpstan-import-type CreateInvoiceRequestArray - * from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest + * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ final class BulkCreateInvoiceRequest { From 93bdc93eebee2df32388e1a383e6b1153c3c0865 Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sat, 23 May 2026 23:26:25 +0300 Subject: [PATCH 10/11] fix: minor cs warning fixes --- .../Internal/Create/BulkCreateInvoiceSaloonRequest.php | 1 + src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php | 1 + src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php | 6 +++++- src/Invoice/Internal/InvoiceResource.php | 4 +++- .../Shared/DTOs/Requests/BulkCreateInvoiceRequest.php | 1 + 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php b/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php index 81e829c..3d006af 100644 --- a/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Create/BulkCreateInvoiceSaloonRequest.php @@ -12,6 +12,7 @@ use Saloon\Http\Response; use Saloon\Traits\Body\HasJsonBody; +// phpcs:disable Generic.Files.LineLength /** * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ diff --git a/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php b/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php index fe8c99c..b53b8b6 100644 --- a/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Create/CreateInvoiceSaloonRequest.php @@ -11,6 +11,7 @@ use Saloon\Http\Response; use Saloon\Traits\Body\HasJsonBody; +// phpcs:disable Generic.Files.LineLength /** * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ diff --git a/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php b/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php index 85d21d7..a137935 100644 --- a/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php +++ b/src/Invoice/Internal/Get/ListInvoiceSaloonRequest.php @@ -51,7 +51,11 @@ public function createDtoFromResponse(Response $response): ApiResponse $singleInvoiceResponses = []; $metaResponse = []; - if ($response->successful() && \array_key_exists('invoices', $responseArr) && \array_key_exists('meta', $responseArr)) { + if ( + $response->successful() + && \array_key_exists('invoices', $responseArr) + && \array_key_exists('meta', $responseArr) + ) { foreach ($responseArr['invoices'] as $key => $invResponse) { InvoiceResponseValidator::for($invResponse, "response.invoices[{$key}]")->validate(); $singleInvoiceResponses[] = InvoiceSuccessfulResponse::fromArray($invResponse); diff --git a/src/Invoice/Internal/InvoiceResource.php b/src/Invoice/Internal/InvoiceResource.php index 05fe2ca..dc6678b 100644 --- a/src/Invoice/Internal/InvoiceResource.php +++ b/src/Invoice/Internal/InvoiceResource.php @@ -59,7 +59,9 @@ public function bulkCreate(BulkCreateInvoiceRequest $invoices): ApiResponse */ public function update(UpdateInvoiceRequest $request): ApiResponse { - return $this->connector->send(new UpdateInvoiceSaloonRequest($request->getInvoiceId(), $request->toRequestBodyArray()))->dto(); + return $this->connector->send( + new UpdateInvoiceSaloonRequest($request->getInvoiceId(), $request->toRequestBodyArray()) + )->dto(); } /** diff --git a/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php index 755256b..55738d8 100644 --- a/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php +++ b/src/Invoice/Shared/DTOs/Requests/BulkCreateInvoiceRequest.php @@ -4,6 +4,7 @@ use InvalidArgumentException; +// phpcs:disable Generic.Files.LineLength /** * @phpstan-import-type CreateInvoiceRequestArray from \HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest */ From 92c73c9c1c34f11e0d9582f61e08db833d76f47a Mon Sep 17 00:00:00 2001 From: Abdurrahman Tantawi Date: Sat, 30 May 2026 04:07:57 +0300 Subject: [PATCH 11/11] update readme and other files --- CONTRIBUTING.md | 2 +- README.md | 834 +++++++++++++----- TODO.md | 5 +- .../DTOs/Requests/CreateInvoiceRequest.php | 8 +- 4 files changed, 596 insertions(+), 253 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a81267..9890469 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,4 +44,4 @@ composer cs-fix ## CI Pipeline -A GitHub Actions CI pipeline runs automatically on every push and pull request targeting `main`. It runs all checks across **PHP 8.2, 8.3, 8.4, and 8.5**. +A GitHub Actions CI pipeline runs automatically on every push and pull request targeting `main`. It runs all checks across **PHP 8.3, 8.4, and 8.5**. diff --git a/README.md b/README.md index aee24b2..ef5eab6 100644 --- a/README.md +++ b/README.md @@ -1,219 +1,565 @@ # Moyasar PHP - + A simple, expressive PHP client for the [Moyasar](https://moyasar.com) payment gateway. - -This package provides a clean interface for working with Moyasar's Invoices and Payments APIs in any PHP 8.2+ application. It's framework-agnostic and works great with Laravel, Symfony, or plain PHP. - + +This package provides a clean interface for working with Moyasar's Invoices and Payments APIs in any PHP 8.3+ application. It's framework-agnostic and works great with Laravel, Symfony, or plain PHP. + +## Table of Contents + +- [Requirements](#requirements) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Invoices](#invoices) + - [Create an Invoice](#create-an-invoice) + - [Bulk-Create Invoices](#bulk-create-invoices) + - [List Invoices](#list-invoices) + - [Get Invoice by ID](#get-invoice-by-id) + - [Update an Invoice](#update-an-invoice) + - [Cancel an Invoice](#cancel-an-invoice) + - [Invoice Response Objects](#invoice-response-objects) +- [Payments](#payments) + - [Create a Payment](#create-a-payment) + - [Fetch a Payment](#fetch-a-payment) + - [List Payments](#list-payments) + - [Update a Payment](#update-a-payment) + - [Refund a Payment](#refund-a-payment) + - [Capture an Authorized Payment](#capture-an-authorized-payment) + - [Void a Payment](#void-a-payment) + - [Payment Status Reference](#payment-status-reference) +- [Error Handling](#error-handling) +- [Testing Your Integration](#testing-your-integration) +- [Contributing](#contributing) +- [License](#license) +- [Next Steps](#next-steps) + ## Requirements - -- PHP **8.2+** + +- PHP **8.3+** - [Saloon](https://docs.saloon.dev) `^4.0` - [Saloon Pagination Plugin](https://docs.saloon.dev/docs/the-pagination-plugin) `^2.3` + ## Installation - + Install via Composer: - + ```bash composer require hamoda-dev/moyasar-php ``` - + That's it. No service providers to register, no config files to publish. The package is framework-agnostic — use it in Laravel, Symfony, Slim, or plain PHP. - + ## Quick Start - -Grab your secret key from the [Moyasar dashboard](https://dashboard.moyasar.com), then: - + +1. Grab your secret key from the [Moyasar dashboard](https://dashboard.moyasar.com) +2. Set the BaseUrl and your secret key in `.env` file + +```bash +MOYASAR_BASE_URL=https://api.moyasar.com/v1 +MOYASAR_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxx +``` + +3. Instantiate the moyasar client and create a quick test invoice + ```php use HamodaDev\Moyasar\Moyasar; -use HamodaDev\Moyasar\Invoice\DTO\CreateInvoiceDTO; - -$moyasar = new Moyasar( - baseUrl: 'https://api.moyasar.com/v1', - apiKey: getenv('MOYASAR_SECRET_KEY'), -); - +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest; +use HamodaDev\Moyasar\Shared\Const\Currency; + +$moyasar = new Moyasar(getenv('MOYASAR_BASE_URL'), getenv('MOYASAR_SECRET_KEY')); + // Create an invoice and send the customer to the hosted payment page -$invoice = $moyasar->invoice()->create(new CreateInvoiceDTO( - amount: 2500, // 25.00 SAR — always in the smallest unit - currency: 'SAR', +$response = $moyasar->invoice()->create(new CreateInvoiceRequest( + amount: 25, + currency: Currency::SAR, description: 'Order #1234', callbackUrl: 'https://example.com/webhooks/moyasar', )); - -header("Location: {$invoice->url}"); ``` - -Three lines to take a payment. No Guzzle, no array-shuffling, no JSON decoding. - -### Recommended Environment Setup - -Keep credentials out of your code: - -```env -MOYASAR_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxx -MOYASAR_BASE_URL=https://api.moyasar.com/v1 -``` - + > [!WARNING] > **Never commit API keys to version control.** Use environment variables, a secrets manager, or your framework's config system. Treat your secret key like a password. - + > [!TIP] > Moyasar issues separate **test** (`sk_test_...`) and **live** (`sk_live_...`) keys. Use the test key in development and staging — you can run real payment flows against test cards without charging anyone. - + --- - + ## Invoices - -Invoices are the fastest way to accept a payment: you create one, send the customer to `invoice->url`, and Moyasar handles the entire checkout UI for you. - + +Invoices are the fastest way to accept a payment: you create one, send the customer to the URL from `getUrl()`, and Moyasar handles the entire checkout UI for you. + +All invoice methods return an `ApiResponse`. Call `getResponse()` to access the typed DTO when the request is successful, or `getResponseAsString()` when you need the raw JSON body. + +| Use case | Method | Returned DTO from `getResponse()` | +| --- | --- | --- | +| Create invoice | `$moyasar->invoice()->create($request)` | `InvoiceSuccessfulResponse\|null` | +| Bulk-create invoices | `$moyasar->invoice()->bulkCreate($request)` | `BulkInvoiceSuccessfulResponse\|null` | +| List invoices | `$moyasar->invoice()->list($request)` | `ListInvoiceSuccessfulResponse\|null` | +| Get invoice by ID | `$moyasar->invoice()->get($invoiceId)` | `InvoiceSuccessfulResponse\|null` | +| Update invoice metadata | `$moyasar->invoice()->update($request)` | `InvoiceSuccessfulResponse\|null` | +| Cancel invoice | `$moyasar->invoice()->cancel($invoiceId)` | `InvoiceSuccessfulResponse\|null` | + ### Create an Invoice - + +#### Sending Request + ```php -use HamodaDev\Moyasar\Invoice\DTO\CreateInvoiceDTO; - -$invoice = $moyasar->invoice()->create(new CreateInvoiceDTO( - amount: 2500, - currency: 'SAR', +use DateTimeImmutable; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest; +use HamodaDev\Moyasar\Shared\Const\Currency; + +$response = $moyasar->invoice()->create(new CreateInvoiceRequest( + amount: 25, + currency: Currency::SAR, description: 'Order #1234', callbackUrl: 'https://example.com/webhooks/moyasar', - successUrl: 'https://example.com/payment/success', - backUrl: 'https://example.com/payment/cancel', - metadata: ['order_id' => '1234'], + successUrl: 'https://example.com/webhooks/moyasar', + backUrl: 'https://example.com/webhooks/moyasar', + expiredAt: DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2026-06-04 05:21:17'), + metadata: [ 'client_id' => 'CID12345' ] )); - -echo $invoice->url; // Hosted payment page — redirect your user here -echo $invoice->id; // Store this alongside your order -echo $invoice->status; // "initiated" for a fresh invoice + +echo $response->isSuccessful(); // true if the http status code 200 <= code <= 299 +echo $response->getCode(); // 200 +echo $response->getResponseAsString(); // {...} +print_r($response->getResponse()); // returns an instance of InvoiceSuccessfulResponse if the response is successful, null otherwise ``` - -| Parameter | Type | Required | What it's for | -| --- | --- | --- | --- | -| `amount` | `int` | Yes | Smallest currency unit (halalas for SAR, cents for USD) | -| `currency` | `string` | Yes | ISO 4217 code (`SAR`, `USD`, ...) | -| `description` | `string` | Yes | Shown to the customer on the payment page | -| `callbackUrl` | `?string` | No | Webhook URL — Moyasar POSTs here when payment status changes | -| `successUrl` | `?string` | No | Where to redirect after a successful payment | -| `backUrl` | `?string` | No | Where to redirect if the customer cancels | -| `expiredAt` | `?string` | No | ISO 8601 — invoice auto-expires after this | -| `metadata` | `?array` | No | Arbitrary key-value data — perfect for your internal IDs | - -Prefer building DTOs from incoming request data? Use the array factory: - + +#### Request Object + +| Parameter | Type | Description | +| --- | --- | --- | +| `amount` | `float` | invoiced amount | +| `currency` | `Currency (enum)` | ISO 4217 code (`SAR`, `USD`, ...) | +| `description` | `string` | Shown to the customer on the payment page | +| `callbackUrl` | `string\|null` | Webhook URL — Moyasar POSTs here when payment status changes | +| `successUrl` | `string\|null` | Where to redirect after a successful payment | +| `backUrl` | `string\|null` | Where to redirect if the customer cancels | +| `expiredAt` | `DateTimeImmutable\|null` | ISO 8601 — invoice auto-expires after this | +| `metadata` | `array\|null` | Arbitrary key-value data — perfect for your internal IDs | + +You can also use the factory method to create the request DTO: + ```php -$dto = CreateInvoiceDTO::fromArray($request->validated()); +$dto = CreateInvoiceRequest::create( + amount: 25, + currency: Currency::SAR, + description: 'Order #1234', + // ... +); $invoice = $moyasar->invoice()->create($dto); ``` - -### Retrieve an Invoice - + +#### Returned Response Object + +| Method | Response wrapper | Successful response DTO | +| --- | --- | --- | +| `$moyasar->invoice()->create($request)` | `ApiResponse` | `InvoiceSuccessfulResponse` | + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getId()` | `string` | Invoice ID, usually a UUID string | `e97e9999-a679-4cd7-95d0-69c9c886484e` | +| `getStatus()` | `InvoiceStatus` | Initial invoice status | `InvoiceStatus::INITIATED` | +| `getAmount()` | `int` | Amount in the currency minor unit | `2500` | +| `getAmountAsFloat()` | `float` | Amount converted to major currency units | `25.0` | +| `getCurrency()` | `Currency` | ISO 4217 code | `Currency::SAR` | +| `getDescription()` | `string` | Invoice description | `Order #1234` | +| `getAmountFormat()` | `string` | Human-readable amount | `25.00 SAR` | +| `getUrl()` | `string` | Hosted checkout URL | `https://checkout.moyasar.com/invoices/e97e9999-a679-4cd7-95d0-69c9c886484e?lang=en` | +| `getCreatedAt()` | `DateTimeImmutable` | Creation timestamp | `2026-05-13T16:43:54.509Z` | +| `getUpdatedAt()` | `DateTimeImmutable` | Last update timestamp | `2026-05-13T16:43:54.509Z` | +| `getLogoUrl()` | `string\|null` | Moyasar dashboard profile logo URL | `https://api.moyasar.com/images/default-logo.png` | +| `getMetadata()` | `array` | Metadata attached to the invoice | `['order_id' => '2222']` | +| `getPayments()` | `InvoicePayment[]` | Payment attempts linked to this invoice | `[]` | +| `getCallbackUrl()` | `string\|null` | Webhook URL | `https://example.com/webhooks/moyasar` | +| `getExpiredAt()` | `DateTimeImmutable\|null` | Expiration timestamp | `2038-01-19T03:14:07.000Z` | +| `getBackUrl()` | `string\|null` | Redirect URL when the customer goes back | `https://example.com/cart` | +| `getSuccessUrl()` | `string\|null` | Redirect URL after successful payment | `https://example.com/thanks` | + +Use `getUrl()` from the returned invoice to redirect the customer to Moyasar's hosted checkout. + ```php -$invoice = $moyasar->invoice()->get('invoice_12345'); - -if ($invoice->status === 'paid') { - // Fulfill the order +$apiResponse = $moyasar->invoice()->create($dto); +$invoice = $apiResponse->getResponse(); + +if ($apiResponse->isSuccessful() && $invoice !== null) { + echo $invoice->getUrl(); + echo $invoice->getAmountAsFloat(); } ``` - -### List Invoices (with Pagination) - -Moyasar returns invoices in pages. The SDK's paginator handles page-walking for you — no manual `?page=N` tracking: - + +### Bulk-Create Invoices + +#### Sending Request + ```php -$paginator = $moyasar->invoice()->list()->paginate($moyasar); - -while ($paginator->hasMorePages()) { - foreach ($paginator->items() as $invoice) { - echo "{$invoice->id} — {$invoice->status}\n"; - } - - $paginator = $paginator->nextPage(); +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\BulkCreateInvoiceRequest; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\CreateInvoiceRequest; +use HamodaDev\Moyasar\Shared\Const\Currency; + +$response = $moyasar->invoice()->bulkCreate(new BulkCreateInvoiceRequest([ + new CreateInvoiceRequest(amount: 10, currency: Currency::SAR, description: 'Invoice A'), + new CreateInvoiceRequest(amount: 20, currency: Currency::SAR, description: 'Invoice B'), + new CreateInvoiceRequest(amount: 35, currency: Currency::SAR, description: 'Invoice C'), +])); + +foreach ($response->getResponse()?->getInvoices() ?? [] as $invoice) { + echo $invoice->getUrl(); } ``` - + +#### Request Object + +| Parameter | Type | Description | +| --- | --- | --- | +| `invoices` | `CreateInvoiceRequest[]` | Invoices to create in one API call | + +Each item uses the same fields documented in [Create an Invoice](#create-an-invoice). + +You can also use the factory method to validate the array contents: + +```php +$request = BulkCreateInvoiceRequest::create([ + CreateInvoiceRequest::create(10, Currency::SAR, 'Invoice A'), + CreateInvoiceRequest::create(20, Currency::SAR, 'Invoice B'), +]); +``` + +#### Returned Response Object + +| Method | Response wrapper | Successful response DTO | +| --- | --- | --- | +| `$moyasar->invoice()->bulkCreate($request)` | `ApiResponse` | `BulkInvoiceSuccessfulResponse` | + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getInvoices()` | `InvoiceSuccessfulResponse[]` | Created invoices | `[$invoiceA, $invoiceB]` | + +### List Invoices + +#### Sending Request + +```php +use HamodaDev\Moyasar\Invoice\Internal\Core\Const\InvoiceStatus; +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\ListInvoiceRequest; + +$response = $moyasar->invoice()->list(new ListInvoiceRequest( + page: 1, + status: InvoiceStatus::PAID, + metadataKey: 'order_id', +)); + +$result = $response->getResponse(); + +foreach ($result?->getInvoices() ?? [] as $invoice) { + echo $invoice->getId(); +} + +$nextPage = $result?->getMeta()->getNextPage(); +``` + +#### Request Object + +| Parameter | Type | Description | +| --- | --- | --- | +| `page` | `int\|null` | Page number to retrieve. Defaults to `1` | +| `id` | `string\|null` | Filter by invoice ID | +| `status` | `InvoiceStatus\|null` | Filter by invoice status | +| `after` | `DateTimeImmutable\|null` | Return invoices created after this timestamp | +| `before` | `DateTimeImmutable\|null` | Return invoices created before this timestamp | +| `metadataKey` | `string\|null` | Filter invoices containing this metadata key | + +#### Returned Response Object + +| Method | Response wrapper | Successful response DTO | +| --- | --- | --- | +| `$moyasar->invoice()->list($request)` | `ApiResponse` | `ListInvoiceSuccessfulResponse` | + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getInvoices()` | `InvoiceSuccessfulResponse[]` | Current page of invoices | `[$invoiceA, $invoiceB]` | +| `getMeta()` | `ResponsePaginator` | Pagination metadata | `$result->getMeta()->getCurrentPage() === 1` | + > [!TIP] -> Every item yielded by `items()` is a fully-typed `InvoiceDTO`. Your IDE will autocomplete `->id`, `->status`, `->amount`, and every other field. - +> Every invoice returned by `getInvoices()` is a fully-typed `InvoiceSuccessfulResponse`. Your IDE will autocomplete `getId()`, `getStatus()`, `getAmount()`, and every other getter. + +### Get Invoice by ID + +#### Sending Request + +```php +use HamodaDev\Moyasar\Invoice\Internal\Core\Const\InvoiceStatus; + +$response = $moyasar->invoice()->get('invoice_12345'); +$invoice = $response->getResponse(); + +if ($invoice?->getStatus() === InvoiceStatus::PAID) { + // Fulfill the order +} +``` + +#### Request Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| `invoiceId` | `string` | ID of the invoice to retrieve | + +#### Returned Response Object + +| Method | Response wrapper | Successful response DTO | +| --- | --- | --- | +| `$moyasar->invoice()->get($invoiceId)` | `ApiResponse` | `InvoiceSuccessfulResponse` | + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getId()` | `string` | Invoice ID | `invoice_12345` | +| `getStatus()` | `InvoiceStatus` | Current invoice status | `InvoiceStatus::PAID` | +| `getAmount()` | `int` | Amount in the currency minor unit | `2500` | +| `getAmountAsFloat()` | `float` | Amount converted to major currency units | `25.0` | +| `getCurrency()` | `Currency` | ISO 4217 code | `Currency::SAR` | +| `getDescription()` | `string` | Invoice description | `Order #1234` | +| `getAmountFormat()` | `string` | Human-readable amount | `25.00 SAR` | +| `getUrl()` | `string` | Hosted checkout URL | `https://checkout.moyasar.com/invoices/invoice_12345?lang=en` | +| `getCreatedAt()` | `DateTimeImmutable` | Creation timestamp | `2026-05-13T16:43:54.509Z` | +| `getUpdatedAt()` | `DateTimeImmutable` | Last update timestamp | `2026-05-13T16:43:54.509Z` | +| `getLogoUrl()` | `string\|null` | Moyasar dashboard profile logo URL | `https://api.moyasar.com/images/default-logo.png` | +| `getMetadata()` | `array` | Metadata attached to the invoice | `['order_id' => '2222']` | +| `getPayments()` | `InvoicePayment[]` | Payment attempts linked to this invoice | `[$payment]` | +| `getCallbackUrl()` | `string\|null` | Webhook URL | `https://example.com/webhooks/moyasar` | +| `getExpiredAt()` | `DateTimeImmutable\|null` | Expiration timestamp | `2038-01-19T03:14:07.000Z` | +| `getBackUrl()` | `string\|null` | Redirect URL when the customer goes back | `https://example.com/cart` | +| `getSuccessUrl()` | `string\|null` | Redirect URL after successful payment | `https://example.com/thanks` | + +Use this response to check the latest invoice state, read payment attempts, or redirect to the hosted checkout URL again. + +```php +$invoice = $moyasar->invoice()->get('invoice_12345')->getResponse(); + +foreach ($invoice?->getPayments() ?? [] as $payment) { + echo $payment->getId(); + echo $payment->getStatus()->value; +} +``` + ### Update an Invoice - + +#### Sending Request + Only `metadata` is updatable after creation — use this to attach internal context as your order progresses: - + ```php -use HamodaDev\Moyasar\Invoice\DTO\UpdateInvoiceDTO; - -$moyasar->invoice()->update('invoice_12345', new UpdateInvoiceDTO( +use HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests\UpdateInvoiceRequest; + +$response = $moyasar->invoice()->update(new UpdateInvoiceRequest( + invoiceId: 'invoice_12345', metadata: [ 'order_id' => '1234', - 'fulfilled_at' => now()->toIso8601String(), + 'fulfilled_at' => '2026-06-04T05:21:17.000Z', ], )); + +$invoice = $response->getResponse(); ``` - -### Bulk-Create Invoices - -Need to send 50 invoices for a batch of orders? One request, one round-trip: - + +#### Request Object + +| Parameter | Type | Description | +| --- | --- | --- | +| `invoiceId` | `string` | ID of the invoice to update | +| `metadata` | `array` | Metadata that should replace or update the invoice metadata | + +You can also use the named factory: + ```php -$result = $moyasar->invoice()->bulkCreate([ - new CreateInvoiceDTO(amount: 1000, currency: 'SAR', description: 'Invoice A'), - new CreateInvoiceDTO(amount: 2000, currency: 'SAR', description: 'Invoice B'), - new CreateInvoiceDTO(amount: 3500, currency: 'SAR', description: 'Invoice C'), +$request = UpdateInvoiceRequest::of('invoice_12345', [ + 'order_id' => '1234', ]); ``` - + +#### Returned Response Object + +| Method | Response wrapper | Successful response DTO | +| --- | --- | --- | +| `$moyasar->invoice()->update($request)` | `ApiResponse` | `InvoiceSuccessfulResponse` | + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getId()` | `string` | Updated invoice ID | `invoice_12345` | +| `getStatus()` | `InvoiceStatus` | Current invoice status after update | `InvoiceStatus::INITIATED` | +| `getAmount()` | `int` | Amount in the currency minor unit | `2500` | +| `getAmountAsFloat()` | `float` | Amount converted to major currency units | `25.0` | +| `getCurrency()` | `Currency` | ISO 4217 code | `Currency::SAR` | +| `getDescription()` | `string` | Invoice description | `Order #1234` | +| `getAmountFormat()` | `string` | Human-readable amount | `25.00 SAR` | +| `getUrl()` | `string` | Hosted checkout URL | `https://checkout.moyasar.com/invoices/invoice_12345?lang=en` | +| `getCreatedAt()` | `DateTimeImmutable` | Creation timestamp | `2026-05-13T16:43:54.509Z` | +| `getUpdatedAt()` | `DateTimeImmutable` | Last update timestamp | `2026-05-13T16:50:12.000Z` | +| `getLogoUrl()` | `string\|null` | Moyasar dashboard profile logo URL | `https://api.moyasar.com/images/default-logo.png` | +| `getMetadata()` | `array` | Updated invoice metadata | `['order_id' => '1234']` | +| `getPayments()` | `InvoicePayment[]` | Payment attempts linked to this invoice | `[]` | +| `getCallbackUrl()` | `string\|null` | Webhook URL | `https://example.com/webhooks/moyasar` | +| `getExpiredAt()` | `DateTimeImmutable\|null` | Expiration timestamp | `2038-01-19T03:14:07.000Z` | +| `getBackUrl()` | `string\|null` | Redirect URL when the customer goes back | `https://example.com/cart` | +| `getSuccessUrl()` | `string\|null` | Redirect URL after successful payment | `https://example.com/thanks` | + +The returned invoice includes the updated metadata. + ### Cancel an Invoice - + +#### Sending Request + ```php -$invoice = $moyasar->invoice()->cancel('invoice_12345'); -// $invoice->status === 'canceled' +use HamodaDev\Moyasar\Invoice\Internal\Core\Const\InvoiceStatus; + +$response = $moyasar->invoice()->cancel('invoice_12345'); +$invoice = $response->getResponse(); + +// $invoice?->getStatus() === InvoiceStatus::CANCELED ``` - -### InvoiceDTO Reference - -| Property | Type | Notes | + +#### Request Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| `invoiceId` | `string` | ID of the invoice to cancel | + +#### Returned Response Object + +| Method | Response wrapper | Successful response DTO | | --- | --- | --- | -| `id` | `string` | Unique identifier | -| `status` | `string` | `initiated`, `pending`, `paid`, `expired`, `canceled` | -| `amount` | `int` | Smallest currency unit | -| `currency` | `string` | ISO 4217 | -| `description` | `string` | | -| `url` | `string` | **Hosted payment page — redirect customers here** | -| `amountFormat` | `string` | e.g. `"25.00 SAR"` | -| `logoUrl` | `string` | Your merchant logo | -| `callbackUrl` | `?string` | | -| `successUrl` | `?string` | | -| `backUrl` | `?string` | | -| `expiredAt` | `?string` | | -| `createdAt` | `string` | | -| `updatedAt` | `string` | | -| `metadata` | `array` | | -| `payments` | `array` | Payment attempts linked to this invoice | - -Need the raw Saloon response? It's always available: - +| `$moyasar->invoice()->cancel($invoiceId)` | `ApiResponse` | `InvoiceSuccessfulResponse` | + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getId()` | `string` | Canceled invoice ID | `invoice_12345` | +| `getStatus()` | `InvoiceStatus` | Invoice status after cancellation | `InvoiceStatus::CANCELED` | +| `getAmount()` | `int` | Amount in the currency minor unit | `2500` | +| `getAmountAsFloat()` | `float` | Amount converted to major currency units | `25.0` | +| `getCurrency()` | `Currency` | ISO 4217 code | `Currency::SAR` | +| `getDescription()` | `string` | Invoice description | `Order #1234` | +| `getAmountFormat()` | `string` | Human-readable amount | `25.00 SAR` | +| `getUrl()` | `string` | Hosted checkout URL | `https://checkout.moyasar.com/invoices/invoice_12345?lang=en` | +| `getCreatedAt()` | `DateTimeImmutable` | Creation timestamp | `2026-05-13T16:43:54.509Z` | +| `getUpdatedAt()` | `DateTimeImmutable` | Last update timestamp | `2026-05-13T16:55:33.000Z` | +| `getLogoUrl()` | `string\|null` | Moyasar dashboard profile logo URL | `https://api.moyasar.com/images/default-logo.png` | +| `getMetadata()` | `array` | Metadata attached to the invoice | `['order_id' => '2222']` | +| `getPayments()` | `InvoicePayment[]` | Payment attempts linked to this invoice | `[]` | +| `getCallbackUrl()` | `string\|null` | Webhook URL | `https://example.com/webhooks/moyasar` | +| `getExpiredAt()` | `DateTimeImmutable\|null` | Expiration timestamp | `2038-01-19T03:14:07.000Z` | +| `getBackUrl()` | `string\|null` | Redirect URL when the customer goes back | `https://example.com/cart` | +| `getSuccessUrl()` | `string\|null` | Redirect URL after successful payment | `https://example.com/thanks` | + +The returned invoice should have `InvoiceStatus::CANCELED`. + +### Invoice Response Objects + +#### InvoiceSuccessfulResponse + +Returned by create, get by ID, update, and cancel invoice calls. It is also used for every invoice inside list and bulk-create responses. + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getId()` | `string` | Invoice ID, usually a UUID string | `e97e9999-a679-4cd7-95d0-69c9c886484e` | +| `getStatus()` | `InvoiceStatus` | One of `INITIATED`, `PAID`, `FAILED`, `REFUNDED`, `CANCELED`, `ON_HOLD`, `EXPIRED`, `VOIDED` | `InvoiceStatus::CANCELED` | +| `getAmount()` | `int` | Amount in the currency minor unit | `2500` | +| `getAmountAsFloat()` | `float` | Amount converted to major currency units | `25.0` | +| `getCurrency()` | `Currency` | ISO 4217 code | `Currency::SAR` | +| `getDescription()` | `string` | Invoice description | `Order #1234` | +| `getAmountFormat()` | `string` | Human-readable amount | `25.00 SAR` | +| `getUrl()` | `string` | Hosted checkout URL | `https://checkout.moyasar.com/invoices/e97e9999-a679-4cd7-95d0-69c9c886484e?lang=en` | +| `getCreatedAt()` | `DateTimeImmutable` | Creation timestamp | `2026-05-13T16:43:54.509Z` | +| `getUpdatedAt()` | `DateTimeImmutable` | Last update timestamp | `2026-05-13T16:43:54.509Z` | +| `getLogoUrl()` | `string\|null` | Moyasar dashboard profile logo URL | `https://api.moyasar.com/images/default-logo.png` | +| `getMetadata()` | `array` | Metadata attached to the invoice | `['order_id' => '2222']` | +| `getPayments()` | `InvoicePayment[]` | Payment attempts linked to this invoice | `[]` | +| `getCallbackUrl()` | `string\|null` | Webhook URL | `https://example.com/webhooks/moyasar` | +| `getExpiredAt()` | `DateTimeImmutable\|null` | Expiration timestamp | `2038-01-19T03:14:07.000Z` | +| `getBackUrl()` | `string\|null` | Redirect URL when the customer goes back | `https://example.com/cart` | +| `getSuccessUrl()` | `string\|null` | Redirect URL after successful payment | `https://example.com/thanks` | + +#### BulkInvoiceSuccessfulResponse + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getInvoices()` | `InvoiceSuccessfulResponse[]` | Created invoices | `[$invoiceA, $invoiceB]` | + +#### ListInvoiceSuccessfulResponse + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getInvoices()` | `InvoiceSuccessfulResponse[]` | Current page of invoices | `[$invoiceA, $invoiceB]` | +| `getMeta()` | `ResponsePaginator` | Pagination metadata | `$result->getMeta()->getCurrentPage() === 1` | + +#### ResponsePaginator + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getCurrentPage()` | `int` | Current page number | `1` | +| `getTotalPages()` | `int` | Total number of pages | `5` | +| `getTotalCount()` | `int` | Total number of invoices matching the query | `93` | +| `getNextPage()` | `int\|null` | Next page number, if one exists | `2` | +| `getPrevPage()` | `int\|null` | Previous page number, if one exists | `null` | + +#### InvoicePayment + +`InvoiceSuccessfulResponse::getPayments()` returns any payment attempts already linked to the invoice. + +| Getter | Type | Description | Example | +| --- | --- | --- | --- | +| `getId()` | `string` | Payment ID | `19f68d63-8f2b-4a7f-8f2c-8f1a6a4c8a11` | +| `getStatus()` | `PaymentStatus` | Payment status enum | `PaymentStatus::PAID` | +| `getAmount()` | `int` | Amount in the currency minor unit | `2500` | +| `getAmountAsFloat()` | `float` | Amount converted to major currency units | `25.0` | +| `getFee()` | `int` | Moyasar fee in the currency minor unit | `100` | +| `getFeeAsFloat()` | `float` | Fee converted to major currency units | `1.0` | +| `getCurrency()` | `Currency` | ISO 4217 code | `Currency::SAR` | +| `getRefunded()` | `int` | Refunded amount in the currency minor unit | `0` | +| `getRefundedAsFloat()` | `float` | Refunded amount converted to major currency units | `0.0` | +| `getCaptured()` | `int` | Captured amount in the currency minor unit | `2500` | +| `getCapturedAsFloat()` | `float` | Captured amount converted to major currency units | `25.0` | +| `getAmountFormat()` | `string` | Human-readable amount | `25.00 SAR` | +| `getFeeFormat()` | `string` | Human-readable fee | `1.00 SAR` | +| `getRefundedFormat()` | `string` | Human-readable refunded amount | `0.00 SAR` | +| `getCapturedFormat()` | `string` | Human-readable captured amount | `25.00 SAR` | +| `getIp()` | `string` | Customer IP address | `127.0.0.1` | +| `getSource()` | `InvoicePaymentSource` | Payment source details | Credit card, Apple Pay, STC Pay, or Samsung Pay details | +| `getCreatedAt()` | `DateTimeImmutable` | Creation timestamp | `2026-05-13T16:43:54.509Z` | +| `getUpdatedAt()` | `DateTimeImmutable` | Last update timestamp | `2026-05-13T16:43:54.509Z` | +| `getRefundedAt()` | `DateTimeImmutable\|null` | Refund timestamp, when refunded | `null` | +| `getCapturedAt()` | `DateTimeImmutable\|null` | Capture timestamp, when captured | `2026-05-13T16:45:10.000Z` | +| `getVoidedAt()` | `DateTimeImmutable\|null` | Void timestamp, when voided | `null` | +| `getDescription()` | `string\|null` | Payment description | `Order #1234` | +| `getInvoiceId()` | `string\|null` | Linked invoice ID | `e97e9999-a679-4cd7-95d0-69c9c886484e` | +| `getCallbackUrl()` | `string\|null` | Webhook URL | `https://example.com/webhooks/moyasar` | +| `getMetadata()` | `array\|null` | Payment metadata | `['order_id' => '2222']` | +| `getSplits()` | `InvoicePaymentSplit[]\|null` | Split payment recipients, if any | `[]` | + +Need the raw response body? It's always available from the `ApiResponse` wrapper: + ```php -$invoice = $moyasar->invoice()->get('invoice_12345'); -$response = $invoice->getResponse(); - -$response->status(); // 200 -$response->headers(); // All response headers -$response->body(); // Raw JSON string +$response = $moyasar->invoice()->get('invoice_12345'); + +$response->getCode(); // 200 +$response->isSuccessful(); // true +$response->getResponseAsString(); // Raw JSON string +$response->getResponse(); // InvoiceSuccessfulResponse|null ``` - + --- - + ## Payments - + Invoices are great when you want Moyasar to host the checkout. **Payments** are for when you want full control — your own card form, your own UX, direct charges against a card or token. - + > [!NOTE] > If you're collecting card details directly, make sure your integration is PCI-compliant. For most merchants, **tokenization** (using a saved `token` source) is safer and simpler than passing raw card numbers. - + ### Create a Payment - + ```php use HamodaDev\Moyasar\Payment\DTO\CreatePaymentDTO; use HamodaDev\Moyasar\Payment\DTO\Source\CreditCardSourceDTO; - + $payment = $moyasar->payment()->create(new CreatePaymentDTO( amount: 10000, // 100.00 SAR currency: 'SAR', @@ -234,57 +580,57 @@ $payment = $moyasar->payment()->create(new CreatePaymentDTO( givenId: 'a1168bd1-47a4-4b97-8a50-dd5caaccacf2', applyCoupon: true, )); - + echo $payment->status; // "initiated", "paid", "authorized", ... echo $payment->id; // Store this with your order ``` - + **About 3D Secure:** when `threeDs: true`, the response may include a redirect URL the customer must visit to complete verification. Always check `$payment->status` and any redirect instructions returned by the API before assuming the payment succeeded. - + **About manual capture:** setting `manual: true` authorizes the charge without capturing funds. Use this when you want to verify a payment now but only capture later (e.g. when you ship the item). See [Capture](#capture-an-authorized-payment) below. - + #### Supported Source Types - -| Source | DTO | Use case | -| --- | --- | --- | -| Credit/debit card | `CreditCardSourceDTO` | Direct card charge | -| Apple Pay | Pass a raw `array` as `source` | Apple Pay token from the browser/app | -| STC Pay | Pass a raw `array` as `source` | STC Pay flow | -| Saved token | `CreditCardSourceDTO` with `token` set | Charging a previously saved card | - + +| Source | DTO | Use case | +| ----------------- | -------------------------------------- | ------------------------------------ | +| Credit/debit card | `CreditCardSourceDTO` | Direct card charge | +| Apple Pay | Pass a raw `array` as `source` | Apple Pay token from the browser/app | +| STC Pay | Pass a raw `array` as `source` | STC Pay flow | +| Saved token | `CreditCardSourceDTO` with `token` set | Charging a previously saved card | + ### Fetch a Payment - + ```php $payment = $moyasar->payment()->get('payment_12345'); - + if ($payment->status === 'paid') { // Mark the order as paid } ``` - + ### List Payments - + Same paginator API as invoices: - + ```php $paginator = $moyasar->payment()->list()->paginate($moyasar); - + while ($paginator->hasMorePages()) { foreach ($paginator->items() as $payment) { echo "{$payment->id} — {$payment->status} — {$payment->amountFormat}\n"; } - + $paginator = $paginator->nextPage(); } ``` - + ### Update a Payment - + Update `description` or `metadata` after the fact — handy for enriching records once your internal workflow catches up: - + ```php use HamodaDev\Moyasar\Payment\DTO\UpdatePaymentDTO; - + $moyasar->payment()->update('payment_12345', new UpdatePaymentDTO( description: 'Kindle Paperwhite — refurbished', metadata: [ @@ -293,136 +639,136 @@ $moyasar->payment()->update('payment_12345', new UpdatePaymentDTO( ], )); ``` - + ### Refund a Payment - + Full refund: - + ```php $payment = $moyasar->payment()->refund('payment_12345'); ``` - + Partial refund (amount in smallest currency unit): - + ```php $payment = $moyasar->payment()->refund('payment_12345', amount: 2500); // Refund 25.00 SAR ``` - + ### Capture an Authorized Payment - + If you created the payment with `manual: true`, capture it when you're ready to actually charge the customer: - + ```php // Full capture $payment = $moyasar->payment()->capture('payment_12345'); - + // Partial capture (e.g. only ship part of an order) $payment = $moyasar->payment()->capture('payment_12345', amount: 5000); ``` - + ### Void a Payment - + Cancel a payment **before** the funds settle in your bank account. Works on `paid`, `authorized`, or `captured` payments — as long as settlement hasn't happened yet. - + ```php $payment = $moyasar->payment()->void('payment_12345'); // $payment->status === 'voided' ``` - + > [!TIP] -> **Void vs. refund:** void *prevents* the money from leaving the customer's account; refund *returns* money that's already moved. Void is cheaper and faster — always prefer it when available. - +> **Void vs. refund:** void _prevents_ the money from leaving the customer's account; refund _returns_ money that's already moved. Void is cheaper and faster — always prefer it when available. + ### Payment Status Reference - -| Status | What it means | -| --- | --- | -| `initiated` | Payment created, customer hasn't paid yet | -| `paid` | Payment succeeded — you can fulfill the order | -| `failed` | Payment failed — check `message` on the DTO for the reason | -| `authorized` | Card authorized but not charged — needs `capture()` | -| `captured` | Authorized payment has been successfully captured | -| `refunded` | Payment refunded (full or partial) | -| `voided` | Payment canceled before settlement | -| `verified` | Card verified during tokenization (no charge made) | - + +| Status | What it means | +| ------------ | ---------------------------------------------------------- | +| `initiated` | Payment created, customer hasn't paid yet | +| `paid` | Payment succeeded — you can fulfill the order | +| `failed` | Payment failed — check `message` on the DTO for the reason | +| `authorized` | Card authorized but not charged — needs `capture()` | +| `captured` | Authorized payment has been successfully captured | +| `refunded` | Payment refunded (full or partial) | +| `voided` | Payment canceled before settlement | +| `verified` | Card verified during tokenization (no charge made) | + --- - + ## Error Handling - + Saloon throws `RequestException` on any non-2xx response. The response object is attached, so you get full context: - + ```php use Saloon\Exceptions\Request\RequestException; use Saloon\Exceptions\Request\FatalRequestException; - + try { $payment = $moyasar->payment()->get('invalid_id'); } catch (RequestException $e) { $status = $e->getResponse()->status(); $body = $e->getResponse()->json(); - + // Moyasar returns structured errors $type = $body['type'] ?? null; // e.g. "invalid_request_error" $message = $body['message'] ?? null; // Human-readable summary $errors = $body['errors'] ?? []; // Field-level validation errors - + report($e); } catch (FatalRequestException $e) { // Network failure — didn't even reach Moyasar report($e); } ``` - + ### Moyasar Error Types - -| Type | Meaning | What to do | -| --- | --- | --- | -| `invalid_request_error` | You sent bad parameters | Check the `errors` field, fix, retry | -| `authentication_error` | Invalid API key | Verify your secret key and base URL | -| `rate_limit_error` | Too many requests | Back off and retry with exponential delay | -| `api_connection_error` | Couldn't reach Moyasar | Retry with backoff | -| `account_inactive_error` | Account not activated for live payments | Contact Moyasar sales | -| `3ds_auth_error` | 3D Secure failed | Ask the customer to try again | -| `api_error` | Something else went wrong | Retry; contact support if it persists | - + +| Type | Meaning | What to do | +| ------------------------ | --------------------------------------- | ----------------------------------------- | +| `invalid_request_error` | You sent bad parameters | Check the `errors` field, fix, retry | +| `authentication_error` | Invalid API key | Verify your secret key and base URL | +| `rate_limit_error` | Too many requests | Back off and retry with exponential delay | +| `api_connection_error` | Couldn't reach Moyasar | Retry with backoff | +| `account_inactive_error` | Account not activated for live payments | Contact Moyasar sales | +| `3ds_auth_error` | 3D Secure failed | Ask the customer to try again | +| `api_error` | Something else went wrong | Retry; contact support if it persists | + ### HTTP Status Codes - -| Code | Meaning | -| --- | --- | -| `200` | Success | -| `400` | Bad request — missing or invalid parameters | -| `401` | Unauthorized — API key invalid | -| `403` | Forbidden — credentials lack permission | -| `404` | Resource not found | -| `405` | Method not allowed — account not activated for live | -| `429` | Rate limited | -| `500` / `503` | Moyasar server issue — retry later | - + +| Code | Meaning | +| ------------- | --------------------------------------------------- | +| `200` | Success | +| `400` | Bad request — missing or invalid parameters | +| `401` | Unauthorized — API key invalid | +| `403` | Forbidden — credentials lack permission | +| `404` | Resource not found | +| `405` | Method not allowed — account not activated for live | +| `429` | Rate limited | +| `500` / `503` | Moyasar server issue — retry later | + > [!WARNING] -> Moyasar occasionally returns **`201` with a failure payload** (e.g. bank declines). Don't trust the status code alone — always inspect `$payment->status` or `$invoice->status` on the DTO. - +> Moyasar occasionally returns **`201` with a failure payload** (e.g. bank declines). Don't trust the status code alone — always inspect the payment status or `$invoice->getStatus()` on the DTO. + --- - + ## Testing Your Integration - + Use Moyasar's [test cards](https://docs.moyasar.com/testing) with your `sk_test_...` key. A few quick patterns: - -| Scenario | Card number | -| --- | --- | -| Successful charge | `4111 1111 1111 1111` | -| Declined charge | `4000 0000 0000 0002` | + +| Scenario | Card number | +| ------------------ | --------------------- | +| Successful charge | `4111 1111 1111 1111` | +| Declined charge | `4000 0000 0000 0002` | | 3D Secure required | `4000 0000 0000 3220` | - + Always verify your webhook handler works end-to-end in `test` mode before flipping to live. - + --- - + ## Contributing - + Bug reports and pull requests welcome. If you're adding a new Moyasar endpoint, please follow the existing Resource / Request / DTO pattern — consistency is why this SDK is pleasant to use. - + ## License - + Moyasar PHP is open-sourced software licensed under the [MIT license](LICENSE). ## Next Steps diff --git a/TODO.md b/TODO.md index 26911b0..191c504 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,3 @@ # TODOs -1. Add Tests -2. Enhance the design (no magic constants, better type casting) -3. Setup CI/CD pipeline -4. Decouple the application from the infrastructure (Saloon) +1. Make Moyasar a singleton class diff --git a/src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequest.php b/src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequest.php index f61d905..144bb60 100644 --- a/src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequest.php +++ b/src/Invoice/Shared/DTOs/Requests/CreateInvoiceRequest.php @@ -2,7 +2,7 @@ namespace HamodaDev\Moyasar\Invoice\Shared\DTOs\Requests; -use DateTime; +use DateTimeImmutable; use HamodaDev\Moyasar\Shared\Const\Currency; use InvalidArgumentException; @@ -29,7 +29,7 @@ public function __construct( private ?string $callbackUrl = null, private ?string $successUrl = null, private ?string $backUrl = null, - private ?DateTime $expiredAt = null, + private ?DateTimeImmutable $expiredAt = null, private ?array $metadata = null, ) { } @@ -41,7 +41,7 @@ public static function create( ?string $callbackUrl = null, ?string $successUrl = null, ?string $backUrl = null, - ?DateTime $expiredAt = null, + ?DateTimeImmutable $expiredAt = null, ?array $metadata = null, ): self { return new self( @@ -129,7 +129,7 @@ public function getbackUrl(): ?string return $this->backUrl; } - public function getexpiredAt(): ?DateTime + public function getexpiredAt(): ?DateTimeImmutable { return $this->expiredAt; }