diff --git a/.github/workflows/aqua.yml b/.github/workflows/aqua.yml index 7b4b1520..85b4b632 100644 --- a/.github/workflows/aqua.yml +++ b/.github/workflows/aqua.yml @@ -17,20 +17,20 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: show-progress: false - name: Authenticate to Google Cloud id: gcloud-auth - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 with: token_format: access_token workload_identity_provider: projects/699052769907/locations/global/workloadIdentityPools/github-identity-pool-shared/providers/github-identity-provider-shared # yamllint disable-line service_account: github-gar-alma-php-client@lyrical-carver-335213.iam.gserviceaccount.com - name: Authenticate to Artifact Registry - uses: docker/login-action@v3 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: europe-docker.pkg.dev username: oauth2accesstoken diff --git a/.github/workflows/backport-pull-request.yml b/.github/workflows/backport-pull-request.yml index da739105..53e8e341 100644 --- a/.github/workflows/backport-pull-request.yml +++ b/.github/workflows/backport-pull-request.yml @@ -17,7 +17,7 @@ jobs: steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: develop @@ -28,7 +28,7 @@ jobs: git reset --hard main - name: Create Pull Request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: commit-message: 'chore: backport main to develop' title: Backport main to develop diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 864c15f0..befaf603 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,12 +33,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@ec406be512d7077f68eed36e63f4d91bc006edc4 # 2.35.4 with: php-version: ${{ matrix.php }} tools: composer:v2 diff --git a/.github/workflows/hotfix-pull-request.yml b/.github/workflows/hotfix-pull-request.yml index 9325fc77..e503dfe3 100644 --- a/.github/workflows/hotfix-pull-request.yml +++ b/.github/workflows/hotfix-pull-request.yml @@ -15,18 +15,18 @@ jobs: steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: main - name: Release drafter - uses: release-drafter/release-drafter@v6 + uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 id: release-drafter env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Update release draft - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const { owner, repo } = context.repo; @@ -39,7 +39,7 @@ jobs: }); - name: Update CHANGELOG.md file - uses: stefanzweifel/changelog-updater-action@v1 + uses: stefanzweifel/changelog-updater-action@a938690fad7edf25368f37e43a1ed1b34303eb36 # v1.12.0 with: latest-version: ${{ steps.release-drafter.outputs.tag_name }} release-notes: "### 🐛 Bug Fixes\n ${{ inputs.changelog-message }}\n" @@ -49,7 +49,7 @@ jobs: ./scripts/update-files-with-release-version.sh ${{ steps.release-drafter.outputs.tag_name }} - name: Create Pull Request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: commit-message: 'chore: update version' title: Release ${{ steps.release-drafter.outputs.tag_name }} diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index d3f2d9ee..9d22a6cc 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -11,6 +11,6 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: TimonVS/pr-labeler-action@v5 + - uses: TimonVS/pr-labeler-action@f9c084306ce8b3f488a8f3ee1ccedc6da131d1af # v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index c1156d29..709d8239 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -15,7 +15,7 @@ jobs: steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install taskfile.dev uses: arduino/setup-task@v2 @@ -85,7 +85,7 @@ jobs: https://uploads.github.com/repos/${{ github.repository }}/releases/${{ steps.fetch-release-draft.outputs.id }}/assets?name=alma-php-client.zip - name: Publish Github release - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: # target_commitish is set to refs/heads/develop by release-drafter as we need to retrieve pull requests merged into develop # We need to override it to refs/heads/main to point to the last commit of main branch instead of develop branch @@ -102,7 +102,7 @@ jobs: }); - name: Format release notes for Slack - uses: LoveToKnow/slackify-markdown-action@v1.1.1 + uses: LoveToKnow/slackify-markdown-action@698a1d4d0ff1794152a93c03ee8ca5e03a310d4e # v1.1.1 id: slack-markdown-release-notes with: text: | @@ -110,10 +110,10 @@ jobs: ${{ steps.fetch-release-draft.outputs.body }} - cc <@france.berut> <@khadija.cherif> + cc <@khadija.cherif> - name: Send changelog to Slack - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1 with: method: chat.postMessage token: ${{ secrets.SLACK_RELEASE_CHANGELOG_BOT_TOKEN }} diff --git a/.github/workflows/release-pull-request.yml b/.github/workflows/release-pull-request.yml index 76f6e592..e0c5938e 100644 --- a/.github/workflows/release-pull-request.yml +++ b/.github/workflows/release-pull-request.yml @@ -10,7 +10,7 @@ jobs: steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: main persist-credentials: false @@ -24,7 +24,7 @@ jobs: git reset --hard develop - name: Create release draft - uses: release-drafter/release-drafter@v6 + uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 id: release-drafter with: # release-drafter should be based on develop to get the correct content as pull requests are merged into develop @@ -35,7 +35,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Update CHANGELOG.md - uses: stefanzweifel/changelog-updater-action@v1 + uses: stefanzweifel/changelog-updater-action@a938690fad7edf25368f37e43a1ed1b34303eb36 # v1.12.0 with: latest-version: ${{ steps.release-drafter.outputs.tag_name }} release-notes: ${{ steps.release-drafter.outputs.body }} @@ -47,7 +47,7 @@ jobs: # If using default Github token, the created pull request won't trigger workflows with pull_request event # See https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs - name: Generate Github token to create PR - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 id: github-token with: app-id: ${{ secrets.ALMA_CREATE_TEAM_PRS_APP_ID }} @@ -55,7 +55,7 @@ jobs: repositories: alma-php-client - name: Create Pull Request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: token: ${{ steps.github-token.outputs.token }} commit-message: 'chore: update version' diff --git a/CHANGELOG.md b/CHANGELOG.md index 3938659f..548f4bdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG +## v2.5.1 - 2025-09-01 + +### Changes + +- Add DataExports endpoints (#137) + +#### Contributors + +@Francois-Gomis, @alma-renovate-bot[bot], @carine-bonnafous, @joyet-simon, @martinfobian, @remi-zuffinetti, [alma-renovate-bot[bot]](https://github.com/apps/alma-renovate-bot) and [github-actions[bot]](https://github.com/apps/github-actions) + ## v2.5.0 - 2025-01-30 ### Changes @@ -220,6 +230,7 @@ + ``` * Add fields and docs to the Payment entity * Add a Refund entity and extract refunds data within the Payment entity constructor diff --git a/composer.json b/composer.json index 172a7fa6..fee4689c 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "alma/alma-php-client", "description": "PHP API client for the Alma payments API", - "version": "2.5.0", + "version": "2.5.1", "type": "library", "require": { "php": "^5.6 || ~7.0 || ~7.1 || ~7.2 || ~7.3 || ~7.4 || ~8.0 || ~8.1 || ~8.2 || ~8.3", diff --git a/src/Client.php b/src/Client.php index 938b0c4b..96b3039f 100644 --- a/src/Client.php +++ b/src/Client.php @@ -30,7 +30,7 @@ class Client { - const VERSION = '2.5.0'; + const VERSION = '2.5.1'; const LIVE_MODE = 'live'; const TEST_MODE = 'test'; @@ -70,6 +70,11 @@ class Client * @var Endpoints\Insurance */ public $insurance; + + /** + * @var Endpoints\DataExports + */ + public $dataExports; /*************************/ /** * @var Endpoints\Configuration @@ -155,6 +160,7 @@ private function initEndpoints() $this->shareOfCheckout = new Endpoints\ShareOfCheckout($this->context); $this->webhooks = new Endpoints\Webhooks($this->context); $this->insurance = new Endpoints\Insurance($this->context); + $this->dataExports = new Endpoints\DataExports($this->context); $this->configuration = new Endpoints\Configuration($this->context); } diff --git a/src/Endpoints/DataExports.php b/src/Endpoints/DataExports.php new file mode 100644 index 00000000..99c6a166 --- /dev/null +++ b/src/Endpoints/DataExports.php @@ -0,0 +1,100 @@ + + * @copyright Copyright (c) 2018 Alma / Nabla SAS + * @license https://opensource.org/licenses/MIT The MIT License + * + */ + +namespace Alma\API\Endpoints; + +use Alma\API\Entities\DataExport; +use Alma\API\Exceptions\ParametersException; +use Alma\API\Exceptions\RequestException; +use Alma\API\ParamsError; +use Alma\API\RequestError; + +class DataExports extends Base +{ + const DATA_EXPORTS_PATH = '/v1/data-exports'; + const ACCEPTED_FORMAT = ['csv', 'xlsx']; + + /** + * @param $data + * + * @return DataExport + * + * @throws RequestException|RequestError + */ + public function create($data) + { + $res = $this->request(self::DATA_EXPORTS_PATH)->setRequestBody($data)->post(); + + if ($res->isError()) { + throw new RequestException($res->errorMessage, null, $res); + } + + return new DataExport($res->json); + } + + /** + * @param $reportId + * + * @return DataExport + * + * @throws RequestException|RequestError + */ + public function fetch($reportId) + { + $res = $this->request(self::DATA_EXPORTS_PATH . '/' . $reportId)->get(); + + if ($res->isError()) { + throw new RequestException($res->errorMessage, null, $res); + } + + return new DataExport($res->json); + } + + /** + * @param $reportId + * + * @param string $format only csv or xlsx + * + * @return mixed + * + * @throws RequestException|RequestError|ParametersException + */ + public function download($reportId, $format) + { + if (!in_array($format, self::ACCEPTED_FORMAT)) { + throw new ParametersException("Invalid format: $format. Accepted format are: " . implode(', ', self::ACCEPTED_FORMAT)); + } + + $res = $this->request(self::DATA_EXPORTS_PATH . '/' . $reportId) + ->setQueryParams(['format' => $format]) + ->get(); + + if ($res->isError()) { + throw new RequestException($res->errorMessage, null, $res); + } + + return $res->responseFile; + } +} diff --git a/src/Entities/DataExport.php b/src/Entities/DataExport.php new file mode 100644 index 00000000..3cf90421 --- /dev/null +++ b/src/Entities/DataExport.php @@ -0,0 +1,79 @@ + + * @copyright Copyright (c) 2018 Alma / Nabla SAS + * @license https://opensource.org/licenses/MIT The MIT License + */ + +namespace Alma\API\Entities; + +class DataExport extends Base +{ + /** @var bool */ + public $complete; + + /** @var int Timestamp */ + public $created; + + /** @var int Timestamp */ + public $end; + + /** @var string */ + public $id; + + /** @var bool */ + public $include_child_accounts; + + /** @var string */ + public $merchant; + + + /** @var int Timestamp */ + public $start; + + /** @var string */ + public $type; + + /** @var string Timestamp */ + public $updated; + + /** @var string */ + public $url_csv; + + /** @var string */ + public $url_pdf; + + /** @var string */ + public $url_xlsx; + + /** @var string */ + public $url_xml; + + /** @var string */ + public $url_zip; + + /** + * @param array $attributes + */ + public function __construct($attributes) + { + parent::__construct($attributes); + } +} diff --git a/src/Response.php b/src/Response.php index ca9a4f9b..c62728da 100644 --- a/src/Response.php +++ b/src/Response.php @@ -29,12 +29,19 @@ class Response { public $responseCode; public $json; + public $responseFile; public $errorMessage; public function __construct($curlHandle, $curlResult) { $this->responseCode = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); - $this->json = json_decode($curlResult, true); + + if ('application/json' === curl_getinfo($curlHandle, CURLINFO_CONTENT_TYPE)) { + $this->json = json_decode($curlResult, true); + } else { + $this->json = null; + $this->responseFile = $curlResult; + } if ($this->isError()) { if ($this->json && array_key_exists('message', $this->json)) { diff --git a/tests/Integration/Endpoints/DataExportsTest.php b/tests/Integration/Endpoints/DataExportsTest.php new file mode 100644 index 00000000..b5f891b6 --- /dev/null +++ b/tests/Integration/Endpoints/DataExportsTest.php @@ -0,0 +1,39 @@ + "payments", + "include_child_accounts" => false + ]; + $dataExport = DataExportsTest::$almaClient->dataExports->create($data); + $this->assertNotNull($dataExport->id); + + for ($i = 0; $i < 5; $i++) { + $fetchedExport = DataExportsTest::$almaClient->dataExports->fetch($dataExport->id); + if ($fetchedExport->complete) { + break; + } + sleep(2); + } + $this->assertEquals($dataExport->id, $fetchedExport->id); + + $downloadedCsvExport = DataExportsTest::$almaClient->dataExports->download($dataExport->id, 'csv'); + $this->assertNotNull($downloadedCsvExport); + } + +} diff --git a/tests/Unit/Endpoints/DataExportsTest.php b/tests/Unit/Endpoints/DataExportsTest.php new file mode 100644 index 00000000..eb689dc7 --- /dev/null +++ b/tests/Unit/Endpoints/DataExportsTest.php @@ -0,0 +1,185 @@ +clientContext = Mockery::mock(ClientContext::class); + $this->dataExportsEndpoint = Mockery::mock(DataExports::class)->makePartial(); + $loggerMock = Mockery::mock(LoggerInterface::class); + $loggerMock->shouldReceive('error'); + $this->requestObject = Mockery::mock(Request::class); + $this->responseMock = Mockery::mock(Response::class); + $this->clientContext->logger = $loggerMock; + $this->dataExportsEndpoint->setClientContext($this->clientContext); + } + + public function tearDown(): void + { + $this->dataExportsEndpoint = null; + $this->responseMock = null; + $this->requestObject = null; + $this->clientContext = null; + Mockery::close(); + } + + /** + * @throws RequestError + * @throws RequestException + */ + public function testCreateDataExportPostThrowRequestException() + { + $this->dataExportsEndpoint->shouldReceive('request') + ->with('/v1/data-exports') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('setRequestBody') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('post') + ->once() + ->andThrow(new RequestException('Request error', null, null)); + + $this->expectException(RequestException::class); + $this->dataExportsEndpoint->create(['type' => 'payments', 'include_child_accounts' => false]); + } + + /** + * @throws RequestError + * @throws RequestException + */ + public function testFetchDataExportGetThrowRequestException() + { + $this->dataExportsEndpoint->shouldReceive('request') + ->with('/v1/data-exports/123') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('get') + ->once() + ->andThrow(new RequestException('Request error', null, null)); + + $this->expectException(RequestException::class); + $this->dataExportsEndpoint->fetch(123); + } + + /** + * @throws RequestException|ParametersException|RequestError + */ + public function testDownloadDataExportGetThrowRequestException() + { + $this->dataExportsEndpoint->shouldReceive('request') + ->with('/v1/data-exports/123') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('setQueryParams') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('get') + ->once() + ->andThrow(new RequestException('Request error', null, null)); + + $this->expectException(RequestException::class); + $this->dataExportsEndpoint->download(123, 'csv'); + } + + /** + * @throws RequestException|ParametersException|RequestError + */ + public function testDownloadDataExportInvalidFormat() + { + $this->expectException(ParametersException::class); + $this->dataExportsEndpoint->download(123, 'pdf'); + } + + /** + * @throws RequestException|RequestError + */ + public function testCreateSuccess() + { + $this->dataExportsEndpoint->shouldReceive('request') + ->with('/v1/data-exports') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('setRequestBody') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('post') + ->once() + ->andReturn($this->responseMock); + + $this->responseMock->shouldReceive('isError')->andReturn(false); + $this->responseMock->json = ['id' => '12345', 'status' => 'completed']; + + $result = $this->dataExportsEndpoint->create(['type' => 'report']); + + $this->assertInstanceOf(DataExport::class, $result); + $this->assertEquals('12345', $result->id); + $this->assertEquals('completed', $result->status); + } + + /** + * @throws RequestException|RequestError + */ + public function testFetchSuccess() + { + $this->dataExportsEndpoint->shouldReceive('request') + ->with('/v1/data-exports/12345') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('get') + ->once() + ->andReturn($this->responseMock); + + $this->responseMock->shouldReceive('isError')->andReturn(false); + $this->responseMock->json = ['id' => '12345', 'status' => 'completed']; + + $result = $this->dataExportsEndpoint->fetch('12345'); + + $this->assertInstanceOf(DataExport::class, $result); + $this->assertEquals('12345', $result->id); + $this->assertEquals('completed', $result->status); + } + + /** + * @throws RequestException|ParametersException|RequestError + */ + public function testDownloadSuccess() + { + $this->dataExportsEndpoint->shouldReceive('request') + ->with('/v1/data-exports/12345') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('setQueryParams') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('get') + ->once() + ->andReturn($this->responseMock); + + $this->responseMock->shouldReceive('isError')->andReturn(false); + $this->responseMock->responseFile = 'file_content'; + + $result = $this->dataExportsEndpoint->download('12345', 'csv'); + + $this->assertEquals('file_content', $result); + } +}