diff --git a/README.md b/README.md index 3b3a7d0..d941bb1 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ $subscription = $subscriptions->create([ ]); $subscription->checkout(); // bill the first period +$subscription->checkout(startsAt: $domain->registered_at); // already running: bill the period that started then $subscription->pause(); // expires when the paid period ends $subscription->resume(); // expired subscriptions start a new period $subscription->revoke(refund: false); // cancel diff --git a/README.stub b/README.stub index a1f5d9d..dc76d3c 100644 --- a/README.stub +++ b/README.stub @@ -111,6 +111,7 @@ $subscription = $subscriptions->create([ ]); $subscription->checkout(); // bill the first period +$subscription->checkout(startsAt: $domain->registered_at); // already running: bill the period that started then $subscription->pause(); // expires when the paid period ends $subscription->resume(); // expired subscriptions start a new period $subscription->revoke(refund: false); // cancel diff --git a/src/Id/Resources/AppSubscription.php b/src/Id/Resources/AppSubscription.php index 3ad1974..fb1c32d 100644 --- a/src/Id/Resources/AppSubscription.php +++ b/src/Id/Resources/AppSubscription.php @@ -4,6 +4,7 @@ namespace Anikeen\Id\Resources; use Anikeen\Id\Exceptions\ApiException; use Anikeen\Id\Support\AppRequest; +use DateTimeInterface; use JsonSerializable; /** @@ -58,11 +59,18 @@ class AppSubscription implements JsonSerializable /** * Activate the subscription and bill its first period. * - * @throws ApiException 409 "already_active", "billing_address_missing", … + * $startsAt (in the past) books a service that is already running, e.g. a domain registered + * earlier: the first period runs from $startsAt, is billed now and renews when it ends. + * + * @throws ApiException 409 "already_active", "billing_address_missing", "period_already_ended", … */ - public function checkout(): self + public function checkout(DateTimeInterface|string|null $startsAt = null): self { - return $this->send('PUT', '/checkout'); + if ($startsAt instanceof DateTimeInterface) { + $startsAt = $startsAt->format(DateTimeInterface::ATOM); + } + + return $this->send('PUT', '/checkout', $startsAt === null ? null : ['starts_at' => $startsAt]); } /** diff --git a/tests/Id/AppSubscriptionsTest.php b/tests/Id/AppSubscriptionsTest.php index d094fe5..53e41ef 100644 --- a/tests/Id/AppSubscriptionsTest.php +++ b/tests/Id/AppSubscriptionsTest.php @@ -76,6 +76,17 @@ class AppSubscriptionsTest extends TestCase $this->assertSame(['refund' => true], $this->sentJson(4)); } + public function test_a_running_service_is_checked_out_from_its_start(): void + { + $this->respond(201, $this->subscription()); + $this->respond(200, $this->subscription(['active' => true])); + + $subscription = $this->owner()->appSubscriptions()->create(['name' => '.de TLD', 'unit' => 'year', 'price' => '5.19']); + $subscription->checkout(new \DateTimeImmutable('2026-05-19 11:33:58', new \DateTimeZone('UTC'))); + + $this->assertSame(['starts_at' => '2026-05-19T11:33:58+00:00'], $this->sentJson(1)); + } + public function test_errors_carry_status_and_error_code(): void { $this->respond(409, ['message' => 'Product subscription has already been cancelled.', 'error' => 'already_cancelled']);