Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "application-manager-tools/am-driver",
"description": "Symfony bundle and framework-agnostic core to integrate managed applications with Application Manager (orchestration, consumption, operational state).",
"type": "symfony-bundle",
"version": "0.0.17",
"version": "0.0.18",
"license": "MIT",
"keywords": [
"application-manager",
Expand Down
32 changes: 32 additions & 0 deletions docs/openapi/am-client-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,38 @@ paths:
'404':
description: Commande inconnue côté AM

/api/v1/instances/{instanceId}/billing/cancel-subscription:
post:
tags:
- AmOutbound
summary: Programmer l’annulation d’abonnement en fin de période (bundle → AM)
operationId: amDriverCancelSubscription
description: >
Implémenté par `AmApiClient::cancelSubscription`. Authentifié par `X-AM-Application-Token`.
parameters:
- name: instanceId
in: path
required: true
schema:
type: string
description: Identifiant instance AM `am_ins_<uuid-v4>`
- name: X-AM-Application-Token
in: header
required: true
schema:
type: string
responses:
'202':
description: Annulation programmée (voir OpenAPI plateforme)
'400':
description: Validation AM
'403':
description: Token invalide ou instance hors périmètre
'404':
description: Instance inconnue
'503':
description: Clé API Stripe non configurée côté AM

components:
schemas:
ConsumptionWebhookEvent:
Expand Down
42 changes: 42 additions & 0 deletions src/Core/Http/AmApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use ApplicationManagerTools\AmDriver\Core\Dto\ConsumptionWebhookEvent;
use ApplicationManagerTools\AmDriver\Core\Dto\OrchestrationCallbackRequest;
use InvalidArgumentException;
use stdClass;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final class AmApiClient implements AmApiClientInterface
Expand Down Expand Up @@ -49,6 +51,46 @@ public function reportOrchestrationCallback(OrchestrationCallbackRequest $reques
);
}

public function cancelSubscription(string $instanceId): array
{
$trimmed = trim($instanceId);
if ('' === $trimmed) {
throw new InvalidArgumentException('instanceId cannot be empty.');
}

$url = $this->config->baseUrl().'/api/v1/instances/'.rawurlencode($trimmed).'/billing/cancel-subscription';

return $this->request(
'POST',
$url,
[
'headers' => $this->applicationHeaders(),
'json' => new stdClass(),
'timeout' => $this->config->timeoutSeconds(),
],
);
}

public function resumeSubscription(string $instanceId): array
{
$trimmed = trim($instanceId);
if ('' === $trimmed) {
throw new InvalidArgumentException('instanceId cannot be empty.');
}

$url = $this->config->baseUrl().'/api/v1/instances/'.rawurlencode($trimmed).'/billing/resume-subscription';

return $this->request(
'POST',
$url,
[
'headers' => $this->applicationHeaders(),
'json' => new stdClass(),
'timeout' => $this->config->timeoutSeconds(),
],
);
}

/**
* @return array<string, string>
*/
Expand Down
14 changes: 14 additions & 0 deletions src/Core/Http/AmApiClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,18 @@ public function pushConsumption(ConsumptionWebhookEvent $event): array;
* @return array{statusCode: int, body: string}
*/
public function reportOrchestrationCallback(OrchestrationCallbackRequest $request): array;

/**
* Programme l’annulation d’abonnement Stripe en fin de période côté AM.
*
* @return array{statusCode: int, body: string}
*/
public function cancelSubscription(string $instanceId): array;

/**
* Annule une résiliation d’abonnement programmée en fin de période côté AM.
*
* @return array{statusCode: int, body: string}
*/
public function resumeSubscription(string $instanceId): array;
}
10 changes: 10 additions & 0 deletions src/Core/Http/NoopAmApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,14 @@ public function reportOrchestrationCallback(OrchestrationCallbackRequest $reques
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}

public function cancelSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true,"data":{"accepted":true,"cancelAtPeriodEnd":true,"periodEnd":"2026-12-31T00:00:00+00:00"}}'];
}

public function resumeSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true,"data":{"accepted":true,"cancelAtPeriodEnd":false,"periodEnd":"2026-12-31T00:00:00+00:00"}}'];
}
}
40 changes: 40 additions & 0 deletions tests/Unit/Http/AmApiClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,46 @@ public function testReportCallbackSerializesArbitraryExtraFieldInJsonBody(): voi
self::assertSame('cl_demo_67mzxiq', $json['integrationInstanceId'] ?? null);
}

public function testCancelSubscriptionPostsToInstanceBillingRoute(): void
{
// Arrange
$recording = new RecordingHttpClient();
$api = new AmApiClient($recording, new AmApiClientConfig('https://am.example', 'secret-app'));
$instanceId = 'am_ins_10000000-0000-4000-8000-000000000001';

// Act
$response = $api->cancelSubscription($instanceId);

// Assert
self::assertSame(202, $response['statusCode']);
self::assertSame('POST', $recording->method);
self::assertSame(
'https://am.example/api/v1/instances/'.$instanceId.'/billing/cancel-subscription',
$recording->url,
);
self::assertSame('secret-app', $this->headerValue($recording->options, 'X-AM-Application-Token'));
}

public function testResumeSubscriptionPostsToInstanceBillingRoute(): void
{
// Arrange
$recording = new RecordingHttpClient();
$api = new AmApiClient($recording, new AmApiClientConfig('https://am.example', 'secret-app'));
$instanceId = 'am_ins_10000000-0000-4000-8000-000000000001';

// Act
$response = $api->resumeSubscription($instanceId);

// Assert
self::assertSame(202, $response['statusCode']);
self::assertSame('POST', $recording->method);
self::assertSame(
'https://am.example/api/v1/instances/'.$instanceId.'/billing/resume-subscription',
$recording->url,
);
self::assertSame('secret-app', $this->headerValue($recording->options, 'X-AM-Application-Token'));
}

/**
* @param array<string, mixed> $options
*/
Expand Down
40 changes: 40 additions & 0 deletions tests/Unit/Orchestration/OrchestrationCommandProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ public function reportOrchestrationCallback($request): array

return ['statusCode' => 202, 'body' => ''];
}

public function cancelSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}

public function resumeSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}
},
new InMemoryLifecycleStore(),
new class implements DeferredCreateInstanceDispatcherInterface {
Expand Down Expand Up @@ -195,6 +205,16 @@ public function reportOrchestrationCallback($request): array

return ['statusCode' => 202, 'body' => ''];
}

public function cancelSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}

public function resumeSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}
},
new InMemoryLifecycleStore(),
new class implements DeferredCreateInstanceDispatcherInterface {
Expand Down Expand Up @@ -387,6 +407,16 @@ public function reportOrchestrationCallback($request): array

return ['statusCode' => 202, 'body' => ''];
}

public function cancelSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}

public function resumeSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}
},
$lifecycle,
new class implements DeferredCreateInstanceDispatcherInterface {
Expand Down Expand Up @@ -491,6 +521,16 @@ public function reportOrchestrationCallback($request): array

return ['statusCode' => 202, 'body' => ''];
}

public function cancelSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}

public function resumeSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}
},
$lifecycleStore ?? new InMemoryLifecycleStore(),
$deferredDispatcher ?? new class implements DeferredCreateInstanceDispatcherInterface {
Expand Down
10 changes: 10 additions & 0 deletions tests/Unit/Snapshot/ConsumptionPublisherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ public function reportOrchestrationCallback(OrchestrationCallbackRequest $reques
{
return ['statusCode' => 202, 'body' => ''];
}

public function cancelSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}

public function resumeSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}
};
$publisher = new ConsumptionPublisher($client, $manager, 'captain-learning');

Expand Down
10 changes: 10 additions & 0 deletions tests/Unit/StateView/SyncStateViewOperationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,16 @@ public function reportOrchestrationCallback($request): array

return ['statusCode' => 202, 'body' => ''];
}

public function cancelSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}

public function resumeSubscription(string $instanceId): array
{
return ['statusCode' => 202, 'body' => '{"success":true}'];
}
},
new class implements OrchestrationCommandLifecycleStoreInterface {
public function isInProgress(string $idempotencyKey): bool
Expand Down
Loading