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 .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ jobs:
composer update --prefer-dist --no-interaction

- name: Run tests
run: vendor/bin/pest
run: vendor/bin/phpunit
1 change: 1 addition & 0 deletions .phpunit.cache/test-results
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":2,"defects":{"Vatly\\Laravel\\Tests\\Models\\SubscriptionSwapAndInvoiceTest::it_passes_additional_options_while_forcing_immediate_flags":5,"Vatly\\Laravel\\Tests\\Models\\SubscriptionSwapAndInvoiceTest::it_overrides_user_provided_immediate_flags":5,"Vatly\\Laravel\\Tests\\Models\\SubscriptionSyncTest::it_syncs_ends_at_when_subscription_is_ended":5,"Vatly\\Laravel\\Tests\\Models\\SubscriptionSyncTest::it_syncs_trial_ends_at_when_present":5},"times":{"Vatly\\Laravel\\Tests\\Builders\\CheckoutBuilderTest::it_instantiates":0.001,"Vatly\\Laravel\\Tests\\Builders\\CheckoutBuilderTest::it_populates_a_minimum_unfiltered_payload":0,"Vatly\\Laravel\\Tests\\Builders\\CheckoutBuilderTest::it_filters_payload_by_default":0.001,"Vatly\\Laravel\\Tests\\Builders\\CheckoutBuilderTest::an_exception_is_thrown_if_no_items_are_provided":0.001,"Vatly\\Laravel\\Tests\\Builders\\CheckoutBuilderTest::it_creates_a_new_checkout":0.004,"Vatly\\Laravel\\Tests\\Builders\\SubscriptionBuilderTest::it_instantiates":0.001,"Vatly\\Laravel\\Tests\\Builders\\SubscriptionBuilderTest::it_can_create_minimum_payload":0.001,"Vatly\\Laravel\\Tests\\Builders\\SubscriptionBuilderTest::it_can_set_the_quantity":0.002,"Vatly\\Laravel\\Tests\\Builders\\SubscriptionBuilderTest::it_can_override_default_redirect_urls":0.001,"Vatly\\Laravel\\Tests\\Events\\Inbound\\SubscriptionWasStartedAtVatlyTest::it_uses_default_subscription_type_when_no_metadata_present":0,"Vatly\\Laravel\\Tests\\Events\\Inbound\\SubscriptionWasStartedAtVatlyTest::it_uses_subscription_type_from_metadata_when_present":0,"Vatly\\Laravel\\Tests\\Events\\Inbound\\SubscriptionWasStartedAtVatlyTest::it_falls_back_to_default_when_vatly_laravel_metadata_is_empty":0,"Vatly\\Laravel\\Tests\\Events\\Inbound\\SubscriptionWasStartedAtVatlyTest::it_extracts_all_fields_correctly":0,"Vatly\\Laravel\\Tests\\Http\\Controllers\\VatlyInboundWebhookControllerTest::it_accepts_a_post_request":0.001,"Vatly\\Laravel\\Tests\\Http\\Controllers\\VatlyInboundWebhookControllerTest::it_handles_unknown_webhook_events":0.001,"Vatly\\Laravel\\Tests\\Http\\Controllers\\VatlyInboundWebhookControllerTest::it_ignores_requests_without_resource_id":0.004,"Vatly\\Laravel\\Tests\\Listeners\\CascadeVatlyWebhookEventsTest::it_cascades_unsupported_events":0.002,"Vatly\\Laravel\\Tests\\Listeners\\CascadeVatlyWebhookEventsTest::it_cascades_subscription_started_events":0.003,"Vatly\\Laravel\\Tests\\Models\\OrderTest::it_implements_order_interface":0.001,"Vatly\\Laravel\\Tests\\Models\\OrderTest::it_can_be_created_with_attributes":0.043,"Vatly\\Laravel\\Tests\\Models\\OrderTest::it_has_a_morph_to_owner_relationship":0.044,"Vatly\\Laravel\\Tests\\Models\\OrderTest::user_can_access_orders_via_relationship":0.045,"Vatly\\Laravel\\Tests\\Models\\OrderTest::is_paid_returns_false_for_non_paid_orders":0,"Vatly\\Laravel\\Tests\\Models\\SubscriptionSwapAndInvoiceTest::it_swaps_plan_with_immediate_invoicing":0.003,"Vatly\\Laravel\\Tests\\Models\\SubscriptionSwapAndInvoiceTest::it_passes_additional_options_while_forcing_immediate_flags":0.003,"Vatly\\Laravel\\Tests\\Models\\SubscriptionSwapAndInvoiceTest::it_overrides_user_provided_immediate_flags":0.026,"Vatly\\Laravel\\Tests\\Models\\SubscriptionSyncTest::it_updates_local_subscription_with_fresh_data_from_vatly":0.002,"Vatly\\Laravel\\Tests\\Models\\SubscriptionSyncTest::it_syncs_ends_at_when_subscription_is_ended":0.004,"Vatly\\Laravel\\Tests\\Models\\SubscriptionSyncTest::it_syncs_trial_ends_at_when_present":0.003,"Vatly\\Laravel\\Tests\\Models\\SubscriptionTest::it_creates_from_webhook_event":0.044,"Vatly\\Laravel\\Tests\\Models\\SubscriptionTest::it_implements_subscription_interface_getters":0.044,"Vatly\\Laravel\\Tests\\Models\\SubscriptionTest::it_identifies_active_subscriptions":0.043,"Vatly\\Laravel\\Tests\\Models\\SubscriptionTest::it_identifies_canceled_subscriptions":0.045,"Vatly\\Laravel\\Tests\\Models\\SubscriptionTest::it_identifies_subscriptions_on_grace_period":0.05,"Vatly\\Laravel\\Tests\\Models\\SubscriptionUpdatePaymentMethodTest::it_returns_the_payment_method_update_url":0.002,"Vatly\\Laravel\\Tests\\Models\\SubscriptionUpdatePaymentMethodTest::it_passes_prefill_data_to_the_action":0.004,"Vatly\\Laravel\\Tests\\VatlyApiActions\\CancelVatlySubscriptionTest::it_initiates":0.003,"Vatly\\Laravel\\Tests\\VatlyApiActions\\CancelVatlySubscriptionTest::it_executes":0.003,"Vatly\\Laravel\\Tests\\VatlyApiActions\\CreateVatlyCheckoutResponseTest::it_redirects":0.003,"Vatly\\Laravel\\Tests\\VatlyApiActions\\CreateVatlyCheckoutTest::it_instantiates":0.002,"Vatly\\Laravel\\Tests\\VatlyApiActions\\CreateVatlyCheckoutTest::it_executes":0.002,"VatlyApiActions\\CreateVatlyCustomerTest::it_instantiates":0.002,"VatlyApiActions\\CreateVatlyCustomerTest::it_executes":0.002,"VatlyApiActions\\GetVatlyCheckoutResponseTest::it_redirects":0.002,"VatlyApiActions\\GetVatlyCheckoutTest::it_instantiates":0.002,"VatlyApiActions\\GetVatlyCheckoutTest::it_executes":0.002,"VatlyApiActions\\GetVatlyCustomerTest::it_instantiates":0.002,"VatlyApiActions\\GetVatlyCustomerTest::it_executes":0.002,"VatlyApiActions\\GetVatlySubscriptionTest::it_instantiates":0.002,"VatlyApiActions\\GetVatlySubscriptionTest::it_executes":0.002,"VatlyApiActions\\SwapVatlySubscriptionPlanTest::it_instantiates":0.002,"VatlyApiActions\\SwapVatlySubscriptionPlanTest::it_executes":0.002}}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ php artisan migrate
4. Add the `Billable` trait to your User model:

```php
use Vatly\Fluent\Contracts\BillableInterface;
use Vatly\Laravel\Contracts\BillableInterface;
use Vatly\Laravel\Billable;

class User extends Authenticatable implements BillableInterface
Expand Down
16 changes: 8 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
"illuminate/http": "^11.0|^12.0",
"illuminate/routing": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"vatly/vatly-fluent-php": "^0.2.0-alpha.2"
"vatly/vatly-fluent-php": "dev-larafast_dev_sprint"
},
"require-dev": {
"larastan/larastan": "^3.9",
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0|^10.0",
"pestphp/pest": "^3.0"
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
Expand All @@ -47,14 +48,13 @@
}
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
"sort-packages": true
},
"scripts": {
"test": "vendor/bin/pest"
"test": "vendor/bin/phpunit",
"analyse": "vendor/bin/phpstan analyse --memory-limit=512M",
"analyze": "@analyse"
},
"minimum-stability": "dev",
"prefer-stable": true
}
}
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ This creates:
Add the `Billable` trait and implement `BillableInterface` on your User model:

```php
use Vatly\Fluent\Contracts\BillableInterface;
use Vatly\Laravel\Contracts\BillableInterface;
use Vatly\Laravel\Billable;

class User extends Authenticatable implements BillableInterface
Expand Down
2 changes: 1 addition & 1 deletion docs/Webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The package includes listeners that automatically handle subscription lifecycle
Listen for Vatly events in your `EventServiceProvider` or using the `Event` facade:

```php
use Vatly\Fluent\Events\SubscriptionStarted;
use Vatly\Laravel\Events\SubscriptionStarted;

Event::listen(SubscriptionStarted::class, function (SubscriptionStarted $event) {
// $event->subscriptionId
Expand Down
66 changes: 66 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
includes:
- vendor/larastan/larastan/extension.neon
# - phpstan-baseline.neon

parameters:
level: 6
paths:
- src
excludePaths:
- vendor
reportUnmatchedIgnoredErrors: false
ignoreErrors:
# Traits used via "use" statements aren't directly instantiated
- '#^Trait .+ is used zero times and is not analysed\.$#'

# BillableInterface is from external package, getMorphClass() is Laravel magic
- '#^Call to an undefined method Vatly\\Fluent\\Contracts\\BillableInterface::getMorphClass\(\)\.$#'

# Static factory methods on exceptions are safe
-
message: '#^Unsafe usage of new static\(\)\.$#'
path: src/Exceptions/*
-
message: '#^Unsafe usage of new static\(\)\.$#'
path: src/VatlyApiActions/*Response.php

# API package returns BaseResource, runtime type is correct
-
message: '#^Parameter .+ expects .+, Vatly\\API\\Resources\\BaseResource.* given\.$#'
path: src/VatlyApiActions/*

# MorphTo covariance is a known Larastan limitation
-
message: '#^Method Vatly\\Laravel\\Models\\Order::owner\(\) should return Illuminate\\Database\\Eloquent\\Relations\\MorphTo.+ but returns .+\.$#'
path: src/Models/Order.php
-
message: '#^Method Vatly\\Laravel\\Models\\Subscription::owner\(\) should return Illuminate\\Database\\Eloquent\\Relations\\MorphTo.+ but returns .+\.$#'
path: src/Models/Subscription.php

# Model uses Billable trait which adds vatlyId() method
-
message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model::vatlyId\(\)\.$#'
path: src/Builders/CheckoutBuilder.php

# Model uses Billable trait which adds vatly_id property
-
message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$vatly_id\.$#'
path: src/Exceptions/CustomerAlreadyCreated.php

# getOwner() returns Model with Billable trait (implements BillableInterface at runtime)
-
message: '#^Method .+::getOwner\(\) should return Vatly\\Fluent\\Contracts\\BillableInterface but returns Illuminate\\Database\\Eloquent\\Model\|null\.$#'
path: src/Models/Order.php

# Static methods called via events (dynamic handler registration)
-
message: '#^Call to an undefined static method Vatly\\Laravel\\Models\\Subscription::handle.+AtVatly\(\)\.$#'
path: src/Listeners/*
-
message: '#^Call to an undefined static method Vatly\\Laravel\\Models\\Subscription::createFromSubscriptionWasStartedAtVatly\(\)\.$#'
path: src/Listeners/StartSubscription.php

# Event method called dynamically
-
message: '#^Call to an undefined method .+::vatlyEventName\(\)\.$#'
path: src/Listeners/LogUnsupportedVatlyWebhookCallReceived.php
6 changes: 5 additions & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="Unit">
<directory>tests</directory>
Expand Down
2 changes: 1 addition & 1 deletion src/Billable.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* Your model should also implement BillableInterface:
*
* ```php
* use Vatly\Fluent\Contracts\BillableInterface;
* use Vatly\Laravel\Contracts\BillableInterface;
* use Vatly\Laravel\Billable;
*
* class User extends Model implements BillableInterface
Expand Down
20 changes: 20 additions & 0 deletions src/Builders/CheckoutBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ class CheckoutBuilder

protected string $redirectUrlCanceled;

/**
* @var array<string, mixed>|null
*/
protected ?array $metadata = null;

/**
* @var Collection<int, mixed>
*/
protected Collection $items;

public function __construct(
Expand All @@ -30,6 +36,10 @@ public function __construct(
$this->items = new Collection;
}

/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
public function payload(array $overrides = [], bool $filtered = true): array
{
$payload = array_merge([
Expand All @@ -44,6 +54,10 @@ public function payload(array $overrides = [], bool $filtered = true): array
return $filtered ? array_filter($payload) : $payload;
}

/**
* @param Collection<int, mixed> $items
* @param array<string, mixed> $payloadOverrides
*/
public function create(
Collection $items,
string $redirectUrlSuccess,
Expand Down Expand Up @@ -82,13 +96,19 @@ public function withRedirectUrlCanceled(string $url): self
return $this;
}

/**
* @param array<string, mixed> $metadata
*/
public function withMetadata(array $metadata): self
{
$this->metadata = $metadata;

return $this;
}

/**
* @param Collection<int, mixed> $items
*/
public function withItems(Collection $items): self
{
$items->each(fn ($item) => $this->items->add($item));
Expand Down
9 changes: 9 additions & 0 deletions src/Builders/SubscriptionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public function withQuantity(int $quantity): self
return $this;
}

/**
* @param array<string, mixed> $checkoutOptions
*/
public function create(array $checkoutOptions = []): CreateVatlyCheckoutResponse
{
return $this
Expand All @@ -71,6 +74,9 @@ public function create(array $checkoutOptions = []): CreateVatlyCheckoutResponse
);
}

/**
* @return array<string, mixed>
*/
public function getSubscriptionPayload(): array
{
return [
Expand All @@ -79,6 +85,9 @@ public function getSubscriptionPayload(): array
];
}

/**
* @return array<string, mixed>
*/
public function getCreateCheckoutPayload(): array
{
return $this->checkoutBuilder->payload();
Expand Down
12 changes: 6 additions & 6 deletions src/Concerns/ManagesCheckouts.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

namespace Vatly\Laravel\Concerns;

use Vatly\Fluent\Actions\CreateCheckout;
use Vatly\Fluent\Builders\CheckoutBuilder;
use Vatly\Fluent\Builders\SubscriptionBuilder;
use Vatly\Fluent\Contracts\ConfigurationInterface;
use Vatly\Laravel\Builders\CheckoutBuilder;
use Vatly\Laravel\Builders\SubscriptionBuilder;
use Vatly\Laravel\VatlyApiActions\CreateVatlyCheckout;
use Vatly\Laravel\VatlyConfig;

trait ManagesCheckouts
{
Expand All @@ -17,7 +17,7 @@ public function checkout(): CheckoutBuilder

return new CheckoutBuilder(
owner: $this,
createCheckout: app()->make(CreateCheckout::class),
createVatlyCheckout: app()->make(CreateVatlyCheckout::class),
);
}

Expand All @@ -26,7 +26,7 @@ public function subscribe(): SubscriptionBuilder
$this->ensureHasVatlyCustomer();

return new SubscriptionBuilder(
config: app()->make(ConfigurationInterface::class),
vatlyConfig: app()->make(VatlyConfig::class),
owner: $this,
checkoutBuilder: $this->checkout(),
);
Expand Down
26 changes: 12 additions & 14 deletions src/Concerns/ManagesCustomer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@

namespace Vatly\Laravel\Concerns;

use Vatly\API\Resources\Customer;
use Vatly\Fluent\Actions\CreateCustomer;
use Vatly\Fluent\Actions\GetCustomer;
use Vatly\Fluent\Actions\Responses\CreateCustomerResponse;
use Vatly\Fluent\Actions\Responses\CustomerResponse;
use Vatly\Fluent\Actions\Responses\GetCustomerResponse;
use Vatly\Fluent\Exceptions\CustomerAlreadyCreatedException;
use Vatly\Fluent\Exceptions\FeatureUnavailableException;
use Vatly\Fluent\Exceptions\InvalidCustomerException;
Expand Down Expand Up @@ -80,7 +78,7 @@ public function assertCustomerExists(): void
}
}

public function createAsVatlyCustomer(array $options = []): CreateCustomerResponse
public function createAsVatlyCustomer(array $options = []): Customer
{
if ($this->hasVatlyId()) {
throw CustomerAlreadyCreatedException::exists($this);
Expand All @@ -94,21 +92,21 @@ public function createAsVatlyCustomer(array $options = []): CreateCustomerRespon
$options['name'] = $name;
}

/** @var CreateCustomerResponse $response */
$response = app()->make(CreateCustomer::class)->execute($options);
/** @var Customer $customer */
$customer = app()->make(CreateCustomer::class)->execute($options);

$this->vatly_id = $response->customerId;
$this->vatly_id = $customer->id;
$this->saveQuietly();

return $response;
return $customer;
}

public function updateVatlyCustomer(array $options = []): CustomerResponse
public function updateVatlyCustomer(array $options = []): Customer
{
throw FeatureUnavailableException::notImplementedOnApi();
}

public function createOrGetVatlyCustomer(array $options = []): CustomerResponse
public function createOrGetVatlyCustomer(array $options = []): Customer
{
if ($this->hasVatlyId()) {
return $this->asVatlyCustomer();
Expand All @@ -126,7 +124,7 @@ public function ensureHasVatlyCustomer(array $createVatlyCustomerOptions = []):
$this->createAsVatlyCustomer($createVatlyCustomerOptions);
}

public function updateOrCreateVatlyCustomer(array $options = []): CustomerResponse
public function updateOrCreateVatlyCustomer(array $options = []): Customer
{
if ($this->hasVatlyId()) {
return $this->updateVatlyCustomer($options);
Expand All @@ -135,7 +133,7 @@ public function updateOrCreateVatlyCustomer(array $options = []): CustomerRespon
return $this->createAsVatlyCustomer($options);
}

public function syncOrCreateVatlyCustomer(array $options = []): CustomerResponse
public function syncOrCreateVatlyCustomer(array $options = []): Customer
{
if ($this->hasVatlyId()) {
return $this->syncVatlyCustomerDetails();
Expand All @@ -144,14 +142,14 @@ public function syncOrCreateVatlyCustomer(array $options = []): CustomerResponse
return $this->createAsVatlyCustomer($options);
}

public function asVatlyCustomer(): GetCustomerResponse
public function asVatlyCustomer(): Customer
{
$this->assertCustomerExists();

return app()->make(GetCustomer::class)->execute($this->vatly_id);
}

public function syncVatlyCustomerDetails(): CustomerResponse
public function syncVatlyCustomerDetails(): Customer
{
return $this->updateVatlyCustomer([
'name' => $this->getVatlyName(),
Expand Down
21 changes: 21 additions & 0 deletions src/Contracts/BillableInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Vatly\Laravel\Contracts;

use Vatly\Fluent\Contracts\BillableInterface as FluentBillableInterface;

/**
* Interface for entities that can be billed (customers).
*
* Implement this interface on your User model or any entity
* that should be able to subscribe and make payments.
*
* @see \Vatly\Laravel\Billable for the trait that provides the implementation
*/
interface BillableInterface extends FluentBillableInterface
{
// Inherits all methods from Fluent BillableInterface
// This wrapper exists so users only need to import from Vatly\Laravel namespace
}
3 changes: 3 additions & 0 deletions src/Events/Inbound/UnsupportedVatlyWebhookCallReceived.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

class UnsupportedVatlyWebhookCallReceived extends BaseAtVatlyEvent
{
/**
* @param array<string, mixed> $object
*/
protected function __construct(
public readonly string $eventName,
public readonly string $resourceId,
Expand Down
Loading