Skip to content

Commit

Permalink
Add support for new Event Meters (#1698)
Browse files Browse the repository at this point in the history
* commit before writing tests

* Add support for new Event Meters

* Updated styles and removed unused imports

* Updated styles and removed unused imports

* Additional style fixes

* removed idea files

* Update src/Subscription.php

* Update src/Subscription.php

* Update src/Subscription.php

* Update src/SubscriptionItem.php

* Update src/SubscriptionItem.php

* Update src/SubscriptionItem.php

* wip

* wip

* wip

* wip

* wip

* Refactor tests

* wip

* wip

* Update ManagesUsageBilling.php

---------

Co-authored-by: Dries Vints <[email protected]>
Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
3 people authored Aug 13, 2024
1 parent 9f37ae8 commit e93c299
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Billable.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Laravel\Cashier\Concerns\ManagesInvoices;
use Laravel\Cashier\Concerns\ManagesPaymentMethods;
use Laravel\Cashier\Concerns\ManagesSubscriptions;
use Laravel\Cashier\Concerns\ManagesUsageBilling;
use Laravel\Cashier\Concerns\PerformsCharges;

trait Billable
Expand All @@ -16,5 +17,6 @@ trait Billable
use ManagesInvoices;
use ManagesPaymentMethods;
use ManagesSubscriptions;
use ManagesUsageBilling;
use PerformsCharges;
}
79 changes: 79 additions & 0 deletions src/Concerns/ManagesUsageBilling.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Laravel\Cashier\Concerns;

use Illuminate\Support\Collection;
use Stripe\Billing\MeterEvent;

trait ManagesUsageBilling
{
/**
* Get all of the defined billing meters.
*
* @param array $options
* @param array $requestOptions
* @return \Illuminate\Support\Collection
*/
public function meters(array $options = [], array $requestOptions = []): Collection
{
return new Collection($this->stripe()->billing->meters->all($options, $requestOptions)->data);
}

/**
* Report usage for a metered product.
*
* @param string $meter
* @param int $quantity
* @param string|null $price
* @param array $options
* @param array $requestOptions
* @return \Stripe\Billing\MeterEvent
*/
public function reportMeterEvent(
string $meter,
int $quantity = 1,
array $options = [],
array $requestOptions = []
): MeterEvent {
$this->assertCustomerExists();

return $this->stripe()->billing->meterEvents->create([
'event_name' => $meter,
'payload' => [
'value' => $quantity,
'stripe_customer_id' => $this->stripeId(),
],
...$options,
], $requestOptions);
}

/**
* Get the usage records for a meter using its ID.
*
* @param string $meterId
* @param array $options
* @param array $requestOptions
* @return \Illuminate\Support\Collection
*/
public function meterEventSummaries(string $meterId, array $options = [], array $requestOptions = []): Collection
{
$this->assertCustomerExists();

$startTime = $options['start_time'] ?? $this->created_at->timestamp;

$endTime = $options['end_time'] ?? time();

unset($options['start_time'], $options['end_time']);

return new Collection($this->stripe()->billing->meters->allEventSummaries(
$meterId,
[
'customer' => $this->stripeId(),
'start_time' => $startTime,
'end_time' => $endTime,
...$options,
],
$requestOptions
)->data);
}
}
8 changes: 8 additions & 0 deletions src/Subscription.php
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,8 @@ public function updateQuantity($quantity, $price = null)
* @param \DateTimeInterface|int|null $timestamp
* @param string|null $price
* @return \Stripe\UsageRecord
*
* @deprecated Migrate to Usage Based billing instead.
*/
public function reportUsage($quantity = 1, $timestamp = null, $price = null)
{
Expand All @@ -559,6 +561,8 @@ public function reportUsage($quantity = 1, $timestamp = null, $price = null)
* @param int $quantity
* @param \DateTimeInterface|int|null $timestamp
* @return \Stripe\UsageRecord
*
* @deprecated Migrate to Usage Based billing instead.
*/
public function reportUsageFor($price, $quantity = 1, $timestamp = null)
{
Expand All @@ -571,6 +575,8 @@ public function reportUsageFor($price, $quantity = 1, $timestamp = null)
* @param array $options
* @param string|null $price
* @return \Illuminate\Support\Collection
*
* @deprecated Migrate to Usage Based billing instead.
*/
public function usageRecords(array $options = [], $price = null)
{
Expand All @@ -587,6 +593,8 @@ public function usageRecords(array $options = [], $price = null)
* @param string $price
* @param array $options
* @return \Illuminate\Support\Collection
*
* @deprecated Migrate to Usage Based billing instead.
*/
public function usageRecordsFor($price, array $options = [])
{
Expand Down
4 changes: 4 additions & 0 deletions src/SubscriptionItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ public function swapAndInvoice($price, array $options = [])
* @param int $quantity
* @param \DateTimeInterface|int|null $timestamp
* @return \Stripe\UsageRecord
*
* @deprecated Migrate to Usage Based billing instead.
*/
public function reportUsage($quantity = 1, $timestamp = null)
{
Expand All @@ -225,6 +227,8 @@ public function reportUsage($quantity = 1, $timestamp = null)
*
* @param array $options
* @return \Illuminate\Support\Collection
*
* @deprecated Migrate to Usage Based billing instead.
*/
public function usageRecords($options = [])
{
Expand Down
188 changes: 188 additions & 0 deletions tests/Feature/UsageBasedBillingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php

namespace Laravel\Cashier\Tests\Feature;

use Exception;
use InvalidArgumentException;

class UsageBasedBillingTest extends FeatureTestCase
{
/**
* @var string
*/
protected static $productId;

/**
* @var string
*/
protected static $meterId;

/**
* @var string
*/
protected static $otherMeterId;

/**
* @var string
*/
protected static $meteredEventPrice;

/**
* @var string
*/
protected static $otherMeteredEventPrice;

/**
* @var string
*/
protected static $meterEventName;

/**
* @var string
*/
protected static $otherMeterEventName;

/**
* @var string
*/
protected static $licensedPrice;

public static function setUpBeforeClass(): void
{
if (! getenv('STRIPE_SECRET')) {
return;
}

parent::setUpBeforeClass();

static::$productId = self::stripe()->products->create([
'name' => 'Laravel Cashier Test Product',
'type' => 'service',
])->id;

self::$meterEventName = 'test-meter-1';
self::$otherMeterEventName = 'test-meter-2';

$meters = self::stripe()->billing->meters->all();

foreach ($meters as $meter) {
if ($meter->event_name === self::$meterEventName && $meter->status === 'active') {
self::stripe()->billing->meters->deactivate($meter->id);
}
if ($meter->event_name === self::$otherMeterEventName && $meter->status === 'active') {
self::stripe()->billing->meters->deactivate($meter->id);
}
}

static::$meterId = self::stripe()->billing->meters->create([
'display_name' => 'example meter 1',
'event_name' => self::$meterEventName,
'default_aggregation' => ['formula' => 'sum'],
'customer_mapping' => [
'type' => 'by_id',
'event_payload_key' => 'stripe_customer_id',
],
])->id;

static::$otherMeterId = self::stripe()->billing->meters->create([
'display_name' => 'example meter 2',
'event_name' => self::$otherMeterEventName,
'default_aggregation' => ['formula' => 'sum'],
'customer_mapping' => [
'type' => 'by_id',
'event_payload_key' => 'stripe_customer_id',
],
])->id;

static::$meteredEventPrice = self::stripe()->prices->create([
'product' => static::$productId,
'nickname' => 'Monthly Metered Event $1 per unit',
'currency' => 'USD',
'recurring' => [
'interval' => 'month',
'usage_type' => 'metered',
'meter' => static::$meterId,
],
'billing_scheme' => 'per_unit',
'unit_amount' => 100,
])->id;

static::$otherMeteredEventPrice = self::stripe()->prices->create([
'product' => static::$productId,
'nickname' => 'Monthly Metered Event $2 per unit',
'currency' => 'USD',
'recurring' => [
'interval' => 'month',
'usage_type' => 'metered',
'meter' => static::$otherMeterId,
],
'billing_scheme' => 'per_unit',
'unit_amount' => 200,
])->id;

static::$licensedPrice = self::stripe()->prices->create([
'product' => static::$productId,
'nickname' => 'Monthly $10 Licensed',
'currency' => 'USD',
'recurring' => [
'interval' => 'month',
],
'unit_amount' => 1000,
])->id;
}

public function test_report_usage_for_meter()
{
$user = $this->createCustomer('test_report_usage_for_meter');

$user->newSubscription('main')
->meteredPrice(static::$meteredEventPrice)
->create('pm_card_visa');

sleep(1);

$user->reportMeterEvent(static::$meterEventName, 10);

$summary = $user->meterEventSummaries(static::$meterId)->first();

$this->assertSame($summary->aggregated_value, 10.0);
}

public function test_reporting_event_usage_for_subscriptions_with_multiple_prices()
{
$user = $this->createCustomer('reporting_usage_for_subscriptions_with_multiple_prices');

$subscription = $user->newSubscription('main', [static::$licensedPrice])
->meteredPrice(static::$meteredEventPrice)
->meteredPrice(static::$otherMeteredEventPrice)
->create('pm_card_visa');

$this->assertSame($subscription->items->count(), 3);

try {
$user->reportMeterEvent(static::$meterEventName);
} catch (Exception $e) {
$this->assertInstanceOf(InvalidArgumentException::class, $e);

$this->assertSame(
'This method requires a price argument since the subscription has multiple prices.', $e->getMessage()
);
}

$user->reportMeterEvent(static::$otherMeterEventName, 20);

try {
$user->meterEventSummaries(static::$otherMeterId)->first();
} catch (Exception $e) {
$this->assertInstanceOf(InvalidArgumentException::class, $e);

$this->assertSame(
'This method requires a price argument since the subscription has multiple prices.', $e->getMessage()
);
}

$summary = $user->meterEventSummaries(static::$otherMeterId)->first();

$this->assertSame($summary->aggregated_value, 20.0);
}
}

0 comments on commit e93c299

Please sign in to comment.