diff --git a/README.md b/README.md
index 6ebcee9..1fd541d 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,96 @@ This is an unofficial PHP SDK for the [Alfa Bank API](https://developers.alfaban
composer require apoloz/alfa-bank-sdk
```
+## Architecture & Data Flow
+
+The SDK abstracts complex banking protocols into a clean, object-oriented interface.
+
+### 1. Unified Request Architecture
+Every request follows a strict "Input DTO -> Request -> Connector -> Output DTO" pipeline.
+
+```mermaid
+graph LR
+ User[Client App] --> DTO_IN[Request DTO]
+ DTO_IN --> REQ[Request Class]
+ REQ --> CON[Connector]
+ CON -->|mTLS + Signature| API((Alfa Bank))
+ API -->|Raw JSON| CON
+ CON --> REQ
+ REQ -->|Data Mapping| DTO_OUT[Response DTO/Collection]
+ DTO_OUT --> User
+
+ style CON fill:#f96,stroke:#333,stroke-width:2px
+ style DTO_IN fill:#bbf,stroke:#333
+ style DTO_OUT fill:#9f6,stroke:#333
+```
+
+### 2. Authentication & Security Flow (BaaS)
+The BaaS API requires a two-step handshake: mTLS for the tunnel and OAuth 2.0 for authorization.
+
+```mermaid
+sequenceDiagram
+ participant App as Client Application
+ participant SDK as AlfaBank SDK
+ participant API as Alfa Bank Gateway
+
+ Note over App, API: 1. Connection (mTLS)
+ SDK->>API: Client Hello (X.509 Certificate)
+ API-->>SDK: Server Hello & Encrypted Tunnel established
+
+ Note over App, API: 2. Authorization (OAuth 2.0)
+ App->>SDK: login(id, secret)
+ SDK->>API: POST /token (Client Credentials)
+ API-->>SDK: Access Token (JWT)
+ SDK->>SDK: Store Token in Authenticator
+
+ Note over App, API: 3. Business Requests
+ App->>SDK: send(CreatePaymentRequest)
+ SDK->>API: POST /payments (Header: Bearer Token)
+ API-->>SDK: 201 Created
+```
+
+### 3. SBP Security (JCS & Body Signing)
+Unlike BaaS, SBP requires signing the actual JSON body using a recursive key-sorting algorithm (JCS).
+
+```mermaid
+flowchart TD
+ A["Raw Data (Array)"] -->|Recursive ksort| B("Canonical Array")
+ B -->|JSON_UNESCAPED_UNICODE| C("JCS JSON String")
+ C -->|RSA-SHA256 Sign| D("Binary Signature")
+ D -->|Base64 Encode| E["'Authorization' Header"]
+ E -->|HTTPS + mTLS| F((Alfa Bank SBP Gateway))
+
+ subgraph SDK Automatics
+ B
+ C
+ D
+ end
+```
+
+### 4. Webhook Pipeline (Laravel Integration)
+The SDK acts as a security firewall for incoming bank notifications.
+
+```mermaid
+sequenceDiagram
+ participant Bank as Alfa Bank
+ participant Mid as VerifyAlfaWebhook (Middleware)
+ participant Fact as WebhookFactory
+ participant Event as AlfaWebhookReceived (Event)
+ participant Listener as Your Listener
+
+ Bank->>Mid: POST /webhooks/alfa
+ Mid->>Mid: 1. Validate Credentials
+ alt Authorized
+ Mid->>Fact: 2. Parse Raw JSON
+ Fact->>Fact: 3. Detect Type (BaaS/SBP)
+ Fact->>Event: 4. Dispatch(Typed DTO)
+ Event->>Listener: 5. Handle Business Logic
+ Mid-->>Bank: 200 OK
+ else Unauthorized
+ Mid-->>Bank: 401 Unauthorized
+ end
+```
+
## Security & Setup (Crucial)
Alfa Bank uses a comprehensive security model requiring **two distinct sets of keys**:
@@ -23,13 +113,13 @@ Alfa Bank uses a comprehensive security model requiring **two distinct sets of k
2. **Signing (JWS/CMS):** To digitally sign the request body.
### Where to get keys?
-- **Sandbox:** Provided by the bank support or downloaded from the Developer Portal (usually as `.pfx` or `.zip`).
+- **Sandbox:** Provided by the bank support or downloaded from the Developer Portal.
- **Production:**
- - **Transport (mTLS):** Generate a private key and CSR using OpenSSL (`openssl genrsa`, `openssl req`). Send the CSR to `alfa_api@alfabank.ru`. The bank will reply with your Certificate.
- - **Signing:** Generated by you (the client). The public certificate must be registered in "Alfa Business Online" or sent to your onboarding manager.
+ - **Transport (mTLS):** Generate a private key and CSR using OpenSSL. Send the CSR to `alfa_api@alfabank.ru`.
+ - **Signing:** Generated by you (the client). Register the public certificate in "Alfa Business Online".
### 1. Converting PFX to PEM
-If the bank provided a `.pfx` file for signing, you must convert it to PEM format:
+If the bank provided a `.pfx` file for signing, convert it to PEM:
```bash
# Extract Private Key
openssl pkcs12 -in signing.pfx -nocerts -out signing_key.pem -nodes
@@ -39,39 +129,21 @@ openssl pkcs12 -in signing.pfx -clcerts -nokeys -out signing_cert.pem
```
### 2. Initialization
-You can now initialize the SDK with both key pairs:
-
```php
use Apoloz\AlfaBank\AlfaBank;
use Apoloz\AlfaBank\Enums\Environment;
$baas = AlfaBank::baas(
- // Transport Keys (mTLS)
certificatePath: '/path/to/transport_cert.pem',
privateKeyPath: '/path/to/transport_key.pem',
-
- // Signing Keys (Business Logic) - Optional, defaults to transport keys if not set
signingCertificatePath: '/path/to/signing_cert.pem',
signingPrivateKeyPath: '/path/to/signing_key.pem',
-
- environment: Environment::SANDBOX,
- caBundlePath: '/path/to/root_ca_bundle.crt'
+ environment: Environment::SANDBOX
);
```
## Core Usage
-### Authentication (BaaS)
-
-Use the `login` method to perform OAuth Client Credentials flow:
-
-```php
-$baas->login(
- clientId: 'your-client-id',
- clientSecret: 'your-client-secret'
-);
-```
-
### Making Requests
#### SBP: Generate QR Code
@@ -79,13 +151,6 @@ $baas->login(
use Apoloz\AlfaBank\Sbp\Requests\GetQrCodeRequest;
use Apoloz\AlfaBank\Sbp\Data\QrCodeRequestData;
-$sbp = AlfaBank::sbp(
- certificatePath: '...',
- privateKeyPath: '...',
- keyNameAlias: '9728109955', // Terminal ID
- signingPrivateKeyPath: '/path/to/signing_key.pem' // Specific signing key for SBP body
-);
-
$response = $sbp->send(new GetQrCodeRequest(
new QrCodeRequestData(TermNo: '...', amount: '100')
));
@@ -93,464 +158,37 @@ $response = $sbp->send(new GetQrCodeRequest(
$link = $response->dto()->payload;
```
-#### Nominal Accounts
-```php
-$response = $baas->send(new CreateBeneficiaryRequest($payload));
-```
-
-### Self-Employed (Payouts & Status)
-
-The SDK supports managing self-employed beneficiaries, checking their FTS status, and processing payouts.
-
-#### Check Taxpayer Status
-```php
-use Apoloz\AlfaBank\Requests\SelfEmployed\GetTaxpayerStatusRequest;
-use Apoloz\AlfaBank\Data\SelfEmployed\TaxpayerStatusRequestData;
-
-$payload = new TaxpayerStatusRequestData(
- externalId: 'unique-id',
- selfemployedVatinList: ['123456789012']
-);
-
-$response = $baas->send(new GetTaxpayerStatusRequest($payload));
-$status = $response->dto()->taxpayerStatusList[0];
-```
-
-#### Process Payout
-```php
-use Apoloz\AlfaBank\Requests\SelfEmployed\CreatePayoutRequest;
-use Apoloz\AlfaBank\Data\SelfEmployed\PayoutRequestData;
-
-$payload = new PayoutRequestData(
- externalId: 'ext-id-456',
- company: ['vatin' => '7700000000'],
- requisite: [
- ['amount' => 1000, 'vatin' => '112233445566']
- ]
-);
-$response = $baas->send(new CreatePayoutRequest($payload));
-```
-
-### File Exchange
-
-Upload and download files securely.
-
-```php
-use Apoloz\AlfaBank\Requests\FileExchange\UploadFileRequest;
-use Apoloz\AlfaBank\Requests\FileExchange\DownloadFileRequest;
-
-// Upload
-$stream = fopen('path/to/file', 'r');
-$baas->send(new UploadFileRequest($uploadUrl, $stream, $fileSize));
-
-// Download
-$response = $baas->send(new DownloadFileRequest($downloadUrl));
-$content = $response->body();
-```
-
-### Payments (RUR Payment Orders)
-
-Create standard payment orders and check their status.
-
+#### Standard Payments
```php
use Apoloz\AlfaBank\Requests\Payments\CreatePaymentRequest;
use Apoloz\AlfaBank\Data\Payments\PaymentData;
-$payload = new PaymentData(
- amount: 100.50,
- date: '2023-10-25',
- externalId: 'order-12345',
- operationCode: '01',
- payeeAccount: '40702...',
- payeeBankBic: '044525225',
- payeeName: 'Partner LLC',
- payerAccount: '40702...',
- payerBankBic: '044525593',
- payerBankCorrAccount: '30101...',
- payerInn: '7700000000',
- payerName: 'My Company',
- priority: '5',
- purpose: 'Payment for services'
-);
-
-// Create Payment
-$baas->send(new CreatePaymentRequest($payload));
-
-// Check State
-use Apoloz\AlfaBank\Requests\Payments\GetPaymentStateRequest;
-$state = $baas->send(new GetPaymentStateRequest('order-12345'))->dto();
-echo $state->bankStatus; // e.g. ACCEPTED
+$payload = new PaymentData(amount: 100.50, ...);
+$response = $baas->send(new CreatePaymentRequest($payload));
+echo $response->dto()->id;
```
-### Cards
+### Advanced Features
-Retrieve a list of client cards.
-
-```php
-use Apoloz\AlfaBank\Requests\Cards\GetCardsRequest;
-
-$response = $baas->send(new GetCardsRequest());
-foreach ($response->dto() as $card) {
- echo $card->maskedNumber . ' - ' . $card->status;
-}
-```
-
-### B2C Payouts (SBP via Phone)
-
-Send money to individuals using their phone number through SBP.
-
-```php
-use Apoloz\AlfaBank\Requests\B2C\CreateB2CPaymentRequest;
-use Apoloz\AlfaBank\Data\B2C\B2CPaymentRequestData;
-use Apoloz\AlfaBank\Data\B2C\B2CClientData;
-use Apoloz\AlfaBank\Data\B2C\B2CPaymentRecipientData;
-
-$payload = new B2CPaymentRequestData(
- client: new B2CClientData(b2cClientId: '...', partnerId: 123),
- inn: '7700000000',
- account: '40702...',
- total: 100.53,
- signedBody: '...base64_signature...',
- payments: B2CPaymentRecipientData::collect([
- [
- 'phone' => '79001112233',
- 'amount' => 100.53,
- 'nspkBankId' => '100000000001',
- 'sequence' => 'unique-seq-1',
- 'name' => ['first' => 'Ivan', 'last' => 'Ivanov']
- ]
- ], DataCollection::class)
-);
-
-$baas->send(new CreateB2CPaymentRequest($payload));
-```
-
-### Payment Registries (Batch Ruble Payments)
-
-Create a registry for multiple ruble payments and sign it.
-
-```php
-use Apoloz\AlfaBank\Requests\Registries\CreateRegistryRequest;
-use Apoloz\AlfaBank\Data\Registries\RegistryRequestData;
-use Apoloz\AlfaBank\Data\Registries\RegistryPaymentData;
-
-$payload = new RegistryRequestData(
- payments: RegistryPaymentData::collect([
- [
- 'amount' => 1000.0,
- 'date' => '2023-11-01',
- 'payeeAccount' => '40702...',
- 'payeeBankBic' => '044525225',
- 'payeeInn' => '7700000000',
- 'payeeName' => 'Vendor LLC',
- 'payerAccount' => '40702...',
- 'purpose' => 'Batch payment part 1'
- ]
- ], DataCollection::class),
- description: 'Weekly payroll'
-);
-
-$response = $baas->send(new CreateRegistryRequest($payload));
-$registryId = $response->json('id');
-
-// Sign Registry
-use Apoloz\AlfaBank\Requests\Registries\SignRegistryRequest;
-use Apoloz\AlfaBank\Data\Registries\RegistrySignatureData;
-use Spatie\LaravelData\DataCollection;
-
-$signatures = RegistrySignatureData::collect([
- [
- 'base64Encoded' => '...', // Base64 signature
- 'signatureType' => 'RSA',
- 'certificateUuid' => '...'
- ]
-], DataCollection::class);
-
-$baas->send(new SignRegistryRequest($registryId, $signatures));
-```
-
-### Customer Info
-
-Get organization profile.
-
-```php
-use Apoloz\AlfaBank\Requests\CustomerInfo\GetCustomerInfoRequest;
-
-$info = $baas->send(new GetCustomerInfoRequest())->dto();
-echo $info->fullName;
-```
-
-### Limits
-
-Get client's operation limits.
-
-```php
-use Apoloz\AlfaBank\Requests\Limits\GetGeneralLimitsRequest;
-
-$response = $baas->send(new GetGeneralLimitsRequest());
-$limits = $response->json();
-```
-
-### Debit Cards
-
-Apply for a new debit card.
-
-```php
-use Apoloz\AlfaBank\Requests\DebitCards\CreateDebitCardApplicationRequest;
-use Apoloz\AlfaBank\Data\DebitCards\DebitCardApplicationData;
-
-$payload = new DebitCardApplicationData(
- embossingName: 'IVAN IVANOV',
- firstName: 'Иван',
- lastName: 'Иванов',
- middleName: 'Иванович',
- mobilePhone: '79001112233',
- passportBirthDate: '1990-01-01',
- sex: 'm',
- sopdConfirmed: true
-);
-
-$baas->send(new CreateDebitCardApplicationRequest($payload));
-```
-
-### User Compliance
-
-Check user compliance for loans.
-
-```php
-use Apoloz\AlfaBank\Requests\Compliance\GetUserComplianceRequest;
-
-$baas->send(new GetUserComplianceRequest());
-```
-
-### DFA (Digital Financial Assets)
-
-List initial placements.
-
-```php
-use Apoloz\AlfaBank\Requests\DFA\GetDfaInitialPlacementListRequest;
-
-$baas->send(new GetDfaInitialPlacementListRequest(limit: 5));
-```
-
-### Currency Rates
-
-Get currency rates by office.
-
-```php
-use Apoloz\AlfaBank\Requests\Rates\GetOfficeRatesRequest;
-
-$baas->send(new GetOfficeRatesRequest());
-```
-
-### Escrow Accounts
-
-Manage construction projects and escrow orders.
-
-```php
-use Apoloz\AlfaBank\Requests\Escrow\GetBuildProjectsRequest;
-
-$projects = $baas->send(new GetBuildProjectsRequest())->json();
-```
-
-### Power of Attorney (Poa)
-
-Request and check status of digital POAs.
-
-```php
-use Apoloz\AlfaBank\Requests\Poa\CreatePoaRequest;
-
-$response = $baas->send(new CreatePoaRequest('poa-123', '7700000000'));
-$uuid = $response->json('requestUuid');
-```
-
-### Loyalty
-
-Access bonus accounts and transaction history.
-
-```php
-use Apoloz\AlfaBank\Requests\Loyalty\GetLoyaltyAccountsRequest;
-
-$accounts = $baas->send(new GetLoyaltyAccountsRequest())->json();
-```
-
-### Reports
-
-Order certificates and reports.
-
-```php
-use Apoloz\AlfaBank\Requests\Reports\CreateAccountBalanceReportRequest;
-
-$baas->send(new CreateAccountBalanceReportRequest(['40702...'], 'eng'));
-```
-
-### Crypto Service
-
-Manage encryption and signing certificates.
-
-```php
-use Apoloz\AlfaBank\Requests\Crypto\GetCertificatesRequest;
-
-$certificates = $baas->send(new GetCertificatesRequest())->json();
-```
-
-### Partner (MMB)
-
-Access partner program data and applications.
-
-```php
-use Apoloz\AlfaBank\Requests\Partner\GetPartnerApplicationsRequest;
-
-$apps = $baas->send(new GetPartnerApplicationsRequest())->json();
-```
-
-### Miscellaneous
-
-- **Bonus Numbers**: `$baas->send(new GetBonusNumbersRequest());`
-- **Credit Offers**: `$baas->send(new GetCreditOffersRequest());`
-- **Self-encashment**: `$baas->send(new CreateSelfEncashmentRegistryRequest($data));`
-- **Business Lounge**: `$baas->send(new GetLoungeBalanceRequest());`
-- **Scoring**: `$baas->send(new GetComplianceScoreRequest('7700000000'));`
-- **Security**: `$baas->send(new GetSubscriptionsRequest());`
-- **Logistics**: `$baas->send(new GetDeliveriesRequest('...'));`
-- **Webhooks**: `$baas->send(new GetWebhooksRequest());`
-- **1C Integration**: `$baas->send(new CheckMerchantRequest('MF123', 'Basic ...'));`
-- **Claims**: `$baas->send(new ResolveClaimRequest('ID', $data));`
-- **Orders (Travel/Afisha)**: `$baas->send(new CreateTravelOrderRequest($data));`
-- **Salary Project**: `$baas->send(new CreatePayrollApplicationRequest($payload));`
-- **Cloud Signature**: `$baas->send(new SubmitSigningRequest('REQ-ID'));`
-- **Aff Integration**: `$baas->send(new CheckComplianceRequest('INN', 'Name'));`
-- **Digital Ruble**: `$sbp->send(new CreateUniQrRequest($uniQrData));`
-
-### Signing Data (CMS)
-
-For requests requiring a digital signature (e.g., Payments), you must canonicalize and sign the data manually.
-
-```php
-use Apoloz\AlfaBank\Utils\CmsSigner;
-use Apoloz\AlfaBank\Data\NominalAccounts\Payouts\DigestSignatureData;
-use Spatie\LaravelData\DataCollection;
-
-// 1. Prepare data
-$fields = [
- 'amount' => 100.50,
- // ... other fields
-];
-
-// 2. Canonicalize & Sign
-$json = CmsSigner::canonicalize($fields);
-$cert = file_get_contents('/path/to/signing_cert.pem');
-$key = file_get_contents('/path/to/signing_key.pem');
-$signature = CmsSigner::sign($json, $cert, $key);
-
-// 3. Attach to DTO
-$paymentData->digestSignatures = DigestSignatureData::collect([
- [
- 'base64Encoded' => $signature,
- 'signatureType' => 'CMS'
- ]
-], DataCollection::class);
-```
-
-## Advanced Features
-
-### Pagination
-The SDK supports automatic pagination for list endpoints:
+#### Pagination
+The SDK supports automatic pagination for all list endpoints:
```php
use Apoloz\AlfaBank\Requests\Accounts\GetAccountsRequest;
$paginator = $baas->paginate(new GetAccountsRequest());
-foreach ($paginator as $account) {
- // Each item is processed automatically
+foreach ($paginator->items() as $account) {
+ echo $account->number; // Typed AccountData object
}
```
-## Laravel Integration
-
-The package supports auto-discovery. Publish the config file:
-
-```bash
-php artisan vendor:publish --tag=alfa-bank-config
-```
-
-Configure your `.env`:
-```env
-ALFA_BAAS_CERT_PATH=/path/to/transport_cert.pem
-# ... other settings
-ALFA_WEBHOOK_USER=your_webhook_user
-ALFA_WEBHOOK_PASSWORD=your_webhook_password
-```
-
-### Webhook Handling (Laravel)
-
-The SDK provides a built-in controller and route macro to handle incoming webhooks and dispatch Laravel events.
-
-1. **Register the route** in `routes/api.php`:
-```php
-Route::alfaWebhooks('/webhooks/alfa');
-```
-
-2. **Listen for the event** in `app/Providers/EventServiceProvider.php`:
-```php
-use Apoloz\AlfaBank\Laravel\Events\AlfaWebhookReceived;
-
-protected $listen = [
- AlfaWebhookReceived::class => [
- \App\Listeners\HandleAlfaWebhook::class,
- ],
-];
-```
-
-3. **Handle the DTO** in your listener:
-```php
-public function handle(AlfaWebhookReceived $event)
-{
- $webhook = $event->webhook; // BaseWebhookData DTO
-
- if ($webhook->object === 'ul_transaction_default') {
- $transaction = $webhook->data;
- // ...
- }
-}
-```
-
-## Error Handling
-
-The SDK throws `Apoloz\AlfaBank\Exceptions\AlfaBankException` when the API returns an error.
-
-```php
-try {
- $baas->send($request);
-} catch (\Apoloz\AlfaBank\Exceptions\AlfaBankException $e) {
- echo $e->getErrorCode(); // e.g. "invalid_token" or "RQ05014"
- echo $e->getMessage();
-}
-```
-
-## Utilities
-
-- **Card PAN Encryption**: `CardEncryptor::encrypt($pan, $pubKey)`
-- **CMS Signing**: `CmsSigner::sign($data, $cert, $key)`
-- **JWS/JWE**: `JwsSigner` and `JweEncryptor` for transport security.
-
-## Diagnostics
-
-### Health Check (Laravel)
-Run the built-in Artisan command to verify your mTLS certificates and connection:
-
+#### Diagnostics
+Run the built-in Artisan command to verify your configuration:
```bash
php artisan alfa:check
```
-### Logging
-Attach a PSR-3 logger to debug requests (sensitive data redacted):
-
-```php
-$baas->withLogging(Log::getLogger());
-```
-
## License
MIT
diff --git a/src/AlfaBank.php b/src/AlfaBank.php
index df7bb5e..48166d6 100644
--- a/src/AlfaBank.php
+++ b/src/AlfaBank.php
@@ -1,11 +1,6 @@
+use Apoloz\AlfaBank\Requests\Accounts\GetAccountsRequest;
-//
-//
class AlfaBank
{
- //
- //
+ /**
+ * Performs a health check on the connection.
+ * Throws an exception if authentication or account listing fails.
+ */
+ public static function check(
+ string $certificatePath,
+ string $privateKeyPath,
+ string $clientId,
+ string $clientSecret,
+ Environment $environment = Environment::SANDBOX,
+ ?string $caBundlePath = null
+ ): bool {
+ $baas = self::baas($certificatePath, $privateKeyPath, $environment, $caBundlePath);
+ $baas->login($clientId, $clientSecret);
+ $baas->send(new GetAccountsRequest());
+ return true;
+ }
+
public static function baas(
string $certificatePath,
string $privateKeyPath,
diff --git a/src/Contracts/EncryptorInterface.php b/src/Contracts/EncryptorInterface.php
index 1d9f54d..d3a1661 100644
--- a/src/Contracts/EncryptorInterface.php
+++ b/src/Contracts/EncryptorInterface.php
@@ -1,5 +1,7 @@
//
use Apoloz\AlfaBank\Contracts\SignerInterface;
-use Apoloz\AlfaBank\Http\Middleware\AlfaResponseMiddleware;
use Apoloz\AlfaBank\Http\Middleware\RequestIdMiddleware;
use Apoloz\AlfaBank\Http\Pagination\AlfaOffsetPaginator;
use Apoloz\AlfaBank\Requests\Auth\GetTokenRequest;
+use Apoloz\AlfaBank\Traits\HasAlfaErrorHandling;
use Saloon\Http\Connector;
use Saloon\Http\Request;
use Saloon\PaginationPlugin\Contracts\HasPagination;
use Saloon\PaginationPlugin\OffsetPaginator;
+use Saloon\Traits\Plugins\AlwaysThrowOnErrors;
use Saloon\Traits\RequestProperties\HasTries;
use Saloon\Traits\Plugins\HasTimeout;
@@ -31,6 +23,8 @@ class AlfaBankConnector extends Connector implements HasPagination
{
use HasTimeout;
use HasTries;
+ use AlwaysThrowOnErrors;
+ use HasAlfaErrorHandling;
public function __construct(
protected AlfaConfig $alfaConfig,
@@ -62,13 +56,14 @@ class AlfaBankConnector extends Connector implements HasPagination
protected function defaultMiddleware(): array
{
- return [];
+ return [
+ new RequestIdMiddleware(),
+ ];
}
public function boot(\Saloon\Http\PendingRequest $pendingRequest): void
{
- $pendingRequest->middleware()->onResponse(new AlfaResponseMiddleware());
- $pendingRequest->middleware()->onRequest(new RequestIdMiddleware());
+ //
}
public function paginate(Request $request): OffsetPaginator
diff --git a/src/Core/AlfaConfig.php b/src/Core/AlfaConfig.php
index 460264a..239e32d 100644
--- a/src/Core/AlfaConfig.php
+++ b/src/Core/AlfaConfig.php
@@ -1,4 +1,6 @@
use Apoloz\AlfaBank\Data\NominalAccounts\Payouts\DigestSignatureData;
use Spatie\LaravelData\Attributes\DataCollectionOf;
+use Spatie\LaravelData\Attributes\Validation\Email;
+use Spatie\LaravelData\Attributes\Validation\Numeric;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataCollection;
//
@@ -36,11 +40,15 @@ class CreateBeneficiaryRequestData extends Data
public ?bool $financialActivities = null,
public ?string $name = null,
public ?string $registrationDate = null,
+ #[Numeric]
public ?string $inn = null,
+ #[Numeric]
public ?string $kpp = null,
+ #[Numeric]
public ?string $ogrn = null,
public ?PersonalInformationData $personalInformation = null,
public ?string $phoneNumber = null,
+ #[Email]
public ?string $email = null,
#[DataCollectionOf(DocumentData::class)]
public ?DataCollection $documents = null,
diff --git a/src/Data/NominalAccounts/Beneficiaries/CreateBeneficiaryResponseData.php b/src/Data/NominalAccounts/Beneficiaries/CreateBeneficiaryResponseData.php
index c08ca2d..60caa2e 100644
--- a/src/Data/NominalAccounts/Beneficiaries/CreateBeneficiaryResponseData.php
+++ b/src/Data/NominalAccounts/Beneficiaries/CreateBeneficiaryResponseData.php
@@ -1,4 +1,6 @@
//
+use Spatie\LaravelData\Attributes\Validation\Numeric;
+use Spatie\LaravelData\Attributes\Validation\Size;
use Spatie\LaravelData\Data;
//
@@ -23,7 +27,9 @@ class PayerInfoData extends Data
public function __construct(
public string $accountNumber,
public string $name,
+ #[Numeric]
public string $inn,
+ #[Numeric, Size(9)]
public string $kpp,
) {}
//
diff --git a/src/Data/Shared/RecipientInfoData.php b/src/Data/Shared/RecipientInfoData.php
index 88ab288..fd30634 100644
--- a/src/Data/Shared/RecipientInfoData.php
+++ b/src/Data/Shared/RecipientInfoData.php
@@ -1,4 +1,6 @@
//
+use Spatie\LaravelData\Attributes\Validation\Numeric;
+use Spatie\LaravelData\Attributes\Validation\Size;
use Spatie\LaravelData\Data;
+
//
//
@@ -23,8 +28,10 @@ class RecipientInfoData extends Data
public function __construct(
public ?string $beneficiaryId = null,
public ?string $accountNumber = null,
+ #[Numeric, Size(9)]
public ?string $bik = null,
public ?string $name = null,
+ #[Numeric]
public ?string $inn = null,
) {}
//
diff --git a/src/Data/Shared/ResultResponseData.php b/src/Data/Shared/ResultResponseData.php
new file mode 100644
index 0000000..a56bbea
--- /dev/null
+++ b/src/Data/Shared/ResultResponseData.php
@@ -0,0 +1,14 @@
+
-//
use Psr\Log\LoggerInterface;
+use Saloon\Contracts\RequestMiddleware;
+use Saloon\Contracts\ResponseMiddleware;
use Saloon\Http\PendingRequest;
use Saloon\Http\Response;
-//
-//
-//
-class LoggingMiddleware
+class LoggingMiddleware implements RequestMiddleware, ResponseMiddleware
{
//
//
diff --git a/src/Http/Middleware/RequestIdMiddleware.php b/src/Http/Middleware/RequestIdMiddleware.php
index 26d5729..ce5401d 100644
--- a/src/Http/Middleware/RequestIdMiddleware.php
+++ b/src/Http/Middleware/RequestIdMiddleware.php
@@ -1,26 +1,17 @@
//
use Apoloz\AlfaBank\Utils\Uuid;
+use Saloon\Contracts\RequestMiddleware;
use Saloon\Http\PendingRequest;
-//
-//
-//
-class RequestIdMiddleware
+class RequestIdMiddleware implements RequestMiddleware
{
- //
- //
public function __invoke(PendingRequest $pendingRequest): void
{
if (!$pendingRequest->headers()->get('X-Request-ID')) {
diff --git a/src/Http/Pagination/AlfaOffsetPaginator.php b/src/Http/Pagination/AlfaOffsetPaginator.php
index 9edfdd4..0a62621 100644
--- a/src/Http/Pagination/AlfaOffsetPaginator.php
+++ b/src/Http/Pagination/AlfaOffsetPaginator.php
@@ -1,5 +1,7 @@
-//
+use Apoloz\AlfaBank\Data\Shared\ResultResponseData;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasJsonBody;
-//
-//
-//
class PayoffAcceptRequest extends Request implements HasBody
{
use HasJsonBody;
- //
- //
protected Method $method = Method::PATCH;
- //
- //
- //
public function __construct(
protected string $payoffId,
protected bool $accept = true
) {}
- //
- //
- //
public function resolveEndpoint(): string
{
return "/api/na/jp/v1/payoffs/{$this->payoffId}/accept";
}
- //
- //
- //
protected function defaultBody(): array
{
return [
'accept' => $this->accept,
];
}
- //
- //
- //
- public function createDtoFromResponse(Response $response): bool
+ public function createDtoFromResponse(Response $response): ResultResponseData
{
- return (bool)($response->json('result') ?? false);
+ return ResultResponseData::from($response->json());
}
- //
-}
+}
\ No newline at end of file
diff --git a/src/Requests/NominalAccounts/Payouts/TaxRefundAcceptRequest.php b/src/Requests/NominalAccounts/Payouts/TaxRefundAcceptRequest.php
index ff437de..2f49203 100644
--- a/src/Requests/NominalAccounts/Payouts/TaxRefundAcceptRequest.php
+++ b/src/Requests/NominalAccounts/Payouts/TaxRefundAcceptRequest.php
@@ -1,61 +1,41 @@
-//
+use Apoloz\AlfaBank\Data\Shared\ResultResponseData;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasJsonBody;
-//
-//
-//
class TaxRefundAcceptRequest extends Request implements HasBody
{
use HasJsonBody;
- //
- //
protected Method $method = Method::PATCH;
- //
- //
- //
public function __construct(
protected string $taxRefundId,
- protected bool $accept
+ protected bool $accept = true
) {}
- //
- //
- //
public function resolveEndpoint(): string
{
return "/api/na/jp/v1/tax-refund/{$this->taxRefundId}/accept";
}
- //
- //
- //
- protected function defaultQuery(): array
+ protected function defaultBody(): array
{
- return ['accept' => $this->accept ? 'true' : 'false'];
+ return [
+ 'accept' => $this->accept,
+ ];
}
- //
- //
- //
- public function createDtoFromResponse(Response $response): bool
+ public function createDtoFromResponse(Response $response): ResultResponseData
{
- return (bool)($response->json('result') ?? false);
+ return ResultResponseData::from($response->json());
}
- //
-}
+}
\ No newline at end of file
diff --git a/src/Requests/NominalAccounts/UniversalRecipients/CreateUniversalRecipientRequest.php b/src/Requests/NominalAccounts/UniversalRecipients/CreateUniversalRecipientRequest.php
index 35c564c..fc2dcb9 100644
--- a/src/Requests/NominalAccounts/UniversalRecipients/CreateUniversalRecipientRequest.php
+++ b/src/Requests/NominalAccounts/UniversalRecipients/CreateUniversalRecipientRequest.php
@@ -1,4 +1,6 @@
-//
use Apoloz\AlfaBank\Sbp\Data\DigitalRuble\CreateUniQrRequestData;
+use Apoloz\AlfaBank\Sbp\Data\DigitalRuble\UniQrResponseData;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
+use Saloon\Http\Response;
use Saloon\Traits\Body\HasJsonBody;
-//
-// use Saloon\Http\Response; // Explicit import would be cleaner
-//
-
-//
-//
class CreateUniQrRequest extends Request implements HasBody
{
use HasJsonBody;
- //
- //
protected Method $method = Method::POST;
- //
- //
- //
public function __construct(
protected CreateUniQrRequestData $data
) {}
- //
- //
public function resolveEndpoint(): string
{
- return ''; // Endpoint is determined by SbpConnector's base URL
+ return '';
}
- //
- //
- //
protected function defaultBody(): array
{
$body = [
@@ -55,7 +35,6 @@ class CreateUniQrRequest extends Request implements HasBody
'uniQrType' => $this->data->uniQrType,
];
- // Conditional mappings
if ($this->data->amount !== null) $body['amount'] = $this->data->amount;
if ($this->data->paymentPurpose !== null) $body['paymentPurpose'] = $this->data->paymentPurpose;
if ($this->data->redirectUrl !== null) $body['redirectUrl'] = $this->data->redirectUrl;
@@ -78,13 +57,9 @@ class CreateUniQrRequest extends Request implements HasBody
return $body;
}
- //
- //
- //
- public function createDtoFromResponse(\Saloon\Http\Response $response): array
+ public function createDtoFromResponse(Response $response): UniQrResponseData
{
- return $response->json();
+ return UniQrResponseData::from($response->json());
}
- //
-}
+}
\ No newline at end of file
diff --git a/src/Sbp/Requests/DigitalRuble/GetUniQrStatusRequest.php b/src/Sbp/Requests/DigitalRuble/GetUniQrStatusRequest.php
index 2145aa0..469242e 100644
--- a/src/Sbp/Requests/DigitalRuble/GetUniQrStatusRequest.php
+++ b/src/Sbp/Requests/DigitalRuble/GetUniQrStatusRequest.php
@@ -1,4 +1,6 @@
middleware()->onResponse(new AlfaResponseMiddleware());
- $pendingRequest->middleware()->onRequest(new RequestIdMiddleware());
-
$bodyRepo = $pendingRequest->body();
if (!$bodyRepo || empty($bodyRepo->all())) {
return;
diff --git a/src/Services/OpenSslCmsSigner.php b/src/Services/OpenSslCmsSigner.php
index b834729..ae7d992 100644
--- a/src/Services/OpenSslCmsSigner.php
+++ b/src/Services/OpenSslCmsSigner.php
@@ -1,4 +1,6 @@
status() >= 400) {
+ return true;
+ }
+
+ $contentType = $response->header('Content-Type') ?? '';
+ if (str_contains($contentType, 'application/json')) {
+ $data = $response->json();
+ if (isset($data['error']) || (isset($data['ErrorCode']) && (string)$data['ErrorCode'] !== '0')) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the custom exception for the failed request.
+ */
+ public function getRequestException(Response $response, ?Throwable $senderException): ?Throwable
+ {
+ $requestId = $response->header('X-Request-ID') ?? 'Unknown';
+ $errorCode = 'UNKNOWN';
+ $errorDescription = 'No description';
+
+ $contentType = $response->header('Content-Type') ?? '';
+ if (str_contains($contentType, 'application/json')) {
+ $data = $response->json();
+ $errorCode = $data['error'] ?? $data['ErrorCode'] ?? 'HTTP_' . $response->status();
+ $errorDescription = $data['error_description'] ?? $data['message'] ?? 'Request failed';
+ } else {
+ $errorCode = 'HTTP_' . $response->status();
+ $errorDescription = 'Request failed with status ' . $response->status();
+ }
+
+ return new AlfaBankException(
+ $response,
+ (string)$errorCode,
+ "({$requestId}) {$errorDescription}"
+ );
+ }
+}
diff --git a/src/Traits/HasAlfaPagination.php b/src/Traits/HasAlfaPagination.php
index 5dff825..dce8a72 100644
--- a/src/Traits/HasAlfaPagination.php
+++ b/src/Traits/HasAlfaPagination.php
@@ -1,4 +1,6 @@
[
[
'number' => '40702810...',
- 'dateCreated' => '2023-01-01',
+ 'openDate' => '2023-01-01',
'type' => 'Checking',
'balance' => ['amount' => 1000]
]
diff --git a/tests/Feature/ErrorHandlingTest.php b/tests/Feature/ErrorHandlingTest.php
index 7e2f141..763eac8 100644
--- a/tests/Feature/ErrorHandlingTest.php
+++ b/tests/Feature/ErrorHandlingTest.php
@@ -19,7 +19,7 @@ test('it throws AlfaBankException on BaaS error', function () {
$connector->withMockClient($mockClient);
$connector->login('id', 'secret');
-})->throws(AlfaBankException::class, 'Alfa Bank API Error: [invalid_client] Bad client secret');
+})->throws(AlfaBankException::class, 'Alfa Bank API Error: [invalid_client] (Unknown) Bad client secret');
test('it throws AlfaBankException on SBP error', function () {
$mockClient = new \Saloon\Http\Faking\MockClient([
@@ -37,4 +37,4 @@ test('it throws AlfaBankException on SBP error', function () {
protected \Saloon\Enums\Method $method = \Saloon\Enums\Method::GET;
public function resolveEndpoint(): string { return '/'; }
});
-})->throws(AlfaBankException::class, 'Alfa Bank API Error: [RQ05014] Invalid amount');
+})->throws(AlfaBankException::class, 'Alfa Bank API Error: [RQ05014] (Unknown) Invalid amount');
diff --git a/tests/Feature/Webhooks/WebhookTest.php b/tests/Feature/Webhooks/WebhookTest.php
index 1abd84e..f1937ca 100644
--- a/tests/Feature/Webhooks/WebhookTest.php
+++ b/tests/Feature/Webhooks/WebhookTest.php
@@ -1,5 +1,7 @@
'ul_transaction_default',
'actionType' => 'create',
'eventTime' => '2022-01-12T00:00:00',
- 'data' => []
+ 'data' => [
+ 'transactionId' => 'TX123',
+ 'amount' => ['amount' => 100, 'currencyName' => 'RUR'],
+ 'direction' => 'DEBIT',
+ 'documentDate' => '2022-01-12',
+ 'operationCode' => '01',
+ 'paymentPurpose' => 'Test',
+ 'uuid' => 'uuid-123'
+ ]
]
];
@@ -18,19 +28,29 @@ test('can detect baas webhook', function () {
$webhook = WebhookFactory::create($payload);
expect($webhook)->toBeInstanceOf(BaasWebhookData::class)
- ->and($webhook->object)->toBe('ul_transaction_default');
+ ->and($webhook->object)->toBe('ul_transaction_default')
+ ->and($webhook->data->transactionId)->toBe('TX123');
});
test('can detect sbp webhook', function () {
$payload = [
- 'command' => 'qrc/notification',
- 'status' => 'ACWP',
- 'qrcId' => 'test-id'
+ [
+ 'object' => 'jp_sbp_incoming_payments',
+ 'actionType' => 'update',
+ 'data' => [
+ 'qrcId' => 'QR123',
+ 'nspkTransactionId' => 'NSPK123',
+ 'amount' => 1000,
+ 'currency' => 'RUR',
+ 'timestamp' => '2024-11-26T09:09:32Z'
+ ]
+ ]
];
expect(WebhookFactory::isSbp($payload))->toBeTrue();
$webhook = WebhookFactory::create($payload);
expect($webhook)->toBeInstanceOf(SbpWebhookData::class)
- ->and($webhook->command)->toBe('qrc/notification');
-});
\ No newline at end of file
+ ->and($webhook->object)->toBe('jp_sbp_incoming_payments')
+ ->and($webhook->data->qrcId)->toBe('QR123');
+});