diff --git a/.idea/blade.xml b/.idea/blade.xml
index 37ac5989..4d7141cd 100644
--- a/.idea/blade.xml
+++ b/.idea/blade.xml
@@ -57,6 +57,7 @@
+
@@ -81,6 +82,7 @@
+
@@ -93,11 +95,13 @@
+
+
@@ -111,6 +115,7 @@
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..576d84cd
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/php-test-framework.xml b/.idea/php-test-framework.xml
new file mode 100644
index 00000000..530f96a3
--- /dev/null
+++ b/.idea/php-test-framework.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/php.xml b/.idea/php.xml
index b210776b..271d04c2 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -147,7 +147,13 @@
+
+
+
+
+
+
diff --git a/src/Subscription.php b/src/Subscription.php
index 416d4fdc..b9e252c7 100644
--- a/src/Subscription.php
+++ b/src/Subscription.php
@@ -8,6 +8,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
use Laravel\Cashier\Concerns\AllowsCoupons;
use Laravel\Cashier\Concerns\HandlesPaymentFailures;
@@ -563,9 +564,12 @@ public function reportUsage($quantity = 1, $timestamp = null, $price = null)
* @return MeterEvent
* @throws ApiErrorException
*/
- public function reportMeterUsage(string $meter, int $quantity = 1, ?string $price = null): MeterEvent
+ public function reportEventUsage(string $meter, int $quantity = 1, ?string $price = null): MeterEvent
{
- return $this->findItemOrFail($price ?? $this->stripe_price)->reportMeterUsage($meter, $quantity);
+ if (! $price) {
+ $this->guardAgainstMultiplePrices();
+ }
+ return $this->findItemOrFail($price ?? $this->stripe_price)->reportEventUsage($meter, $quantity);
}
/**
@@ -591,9 +595,9 @@ public function reportUsageFor($price, $quantity = 1, $timestamp = null)
* @return MeterEvent
* @throws ApiErrorException
*/
- public function reportUsageForMeter(string $meter, string $price, int $quantity = 1): MeterEvent
+ public function reportUsageForEvent(string $eventName, string $price, int $quantity = 1): MeterEvent
{
- return $this->reportMeterUsage($meter, $quantity, $price);
+ return $this->reportEventUsage($eventName, $quantity, $price);
}
/**
@@ -612,6 +616,25 @@ public function usageRecords(array $options = [], $price = null)
return $this->findItemOrFail($price ?? $this->stripe_price)->usageRecords($options);
}
+
+ /**
+ * Get the usage records for a meter using its ID (not name).
+ *
+ * @param string $meterId
+ * @param array $options
+ * @param null $price
+ * @return Collection
+ * @throws ApiErrorException
+ */
+ public function meterUsageRecords(string $meterId, array $options = [], $price = null): Collection
+ {
+ if (! $price) {
+ $this->guardAgainstMultiplePrices();
+ }
+
+ return $this->findItemOrFail($price ?? $this->stripe_price)->eventUsageRecord($meterId, $options);
+ }
+
/**
* Get the usage records for a specific price of a metered product.
*
diff --git a/src/SubscriptionItem.php b/src/SubscriptionItem.php
index 3b906e27..148db80c 100644
--- a/src/SubscriptionItem.php
+++ b/src/SubscriptionItem.php
@@ -231,7 +231,7 @@ public function reportUsage($quantity = 1, $timestamp = null)
* @return MeterEvent
* @throws ApiErrorException
*/
- public function reportMeterUsage(string $meter, int $quantity = 1): MeterEvent
+ public function reportEventUsage(string $meter, int $quantity = 1): MeterEvent
{
return $this->subscription->owner->stripe()->billing->meterEvents->create([
'event_name' => $meter,
@@ -255,6 +255,47 @@ public function usageRecords($options = [])
)->data);
}
+ /**
+ * List all the metered prices for the subscription item.
+ * @see https://stripe.com/docs/api/prices/list
+ *
+ * @param array|null $params
+ * @param array|null $opts
+ *
+ * @return Collection
+ * @throws ApiErrorException
+ */
+ public function listMeters(?array $params = [], ?array $opts = []): Collection
+ {
+ return new Collection($this->subscription->owner->stripe()->billing->meters->all($params, $opts)->data);
+ }
+
+ /**
+ * @param string $meterId
+ * @param array|null $params
+ * @param array|null $opts
+ * @return Collection
+ * @throws ApiErrorException
+ */
+ public function eventUsageRecord(string $meterId, ?array $params = [], ?array $opts = []): Collection
+ {
+ $startTime = $params['start_time'] ?? $this->subscription->created_at->timestamp;
+ $endTime = $params['end_time'] ?? time();
+
+ unset($params['start_time'], $params['end_time']);
+
+ $params = [
+ 'customer' => $this->subscription->owner->stripeId(),
+ 'start_time' => $startTime,
+ 'end_time' => $endTime,
+ ...$params
+ ];
+
+ return new Collection($this->subscription->owner->stripe()->billing->meters->allEventSummaries(
+ $meterId, $params, $opts
+ )->data);
+ }
+
/**
* Update the underlying Stripe subscription item information for the model.
*
diff --git a/tests/Feature/MeteredBillingTest.php b/tests/Feature/MeteredBillingTest.php
index 249ffb96..9d79f87d 100644
--- a/tests/Feature/MeteredBillingTest.php
+++ b/tests/Feature/MeteredBillingTest.php
@@ -4,6 +4,7 @@
use Exception;
use InvalidArgumentException;
+use Stripe\Exception\ApiErrorException;
use Stripe\Exception\InvalidRequestException;
class MeteredBillingTest extends FeatureTestCase
@@ -23,6 +24,36 @@ class MeteredBillingTest extends FeatureTestCase
*/
protected static $otherMeteredPrice;
+ /**
+ * @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
*/
@@ -30,7 +61,7 @@ class MeteredBillingTest extends FeatureTestCase
public static function setUpBeforeClass(): void
{
- if (! getenv('STRIPE_SECRET')) {
+ if (!getenv('STRIPE_SECRET')) {
return;
}
@@ -63,6 +94,68 @@ public static function setUpBeforeClass(): void
'unit_amount' => 200,
])->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',
@@ -93,6 +186,22 @@ public function test_report_usage_for_metered_price()
$this->assertSame($summary->total_usage, 15);
}
+ public function test_report_usage_for_meter()
+ {
+ $user = $this->createCustomer('test_report_usage_for_meter');
+
+ $subscription = $user->newSubscription('main')
+ ->meteredPrice(static::$meteredEventPrice)
+ ->create('pm_card_visa');
+
+ sleep(1);
+ $subscription->reportUsageForEvent(static::$meterEventName, static::$meteredEventPrice, 10);
+
+ $summary = $subscription->meterUsageRecords(static::$meterId)->first();
+
+ $this->assertSame($summary->aggregated_value, 10.0);
+ }
+
public function test_reporting_usage_for_licensed_price_throws_exception()
{
$user = $this->createCustomer('reporting_usage_for_licensed_price_throws_exception');
@@ -106,6 +215,21 @@ public function test_reporting_usage_for_licensed_price_throws_exception()
}
}
+
+ public function test_reporting_usage_for_legacy_metered_price_throws_exception()
+ {
+ $user = $this->createCustomer('reporting_usage_for_licensed_price_throws_exception');
+
+ $subscription = $user->newSubscription('main')->meteredPrice(static::$meteredEventPrice)->create('pm_card_visa');
+
+ try {
+ $subscription->reportUsage();
+ } catch (Exception $e) {
+ $this->assertInstanceOf(InvalidRequestException::class, $e);
+ }
+ }
+
+
public function test_reporting_usage_for_subscriptions_with_multiple_prices()
{
$user = $this->createCustomer('reporting_usage_for_subscriptions_with_multiple_prices');
@@ -140,6 +264,47 @@ public function test_reporting_usage_for_subscriptions_with_multiple_prices()
}
}
+
+ 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 {
+ $subscription->reportEventUsage(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()
+ );
+ }
+
+ $subscription->reportEventUsage(static::$otherMeterEventName, 20, static::$otherMeteredEventPrice);
+
+ try {
+ $subscription->meterUsageRecords(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 = $subscription->meterUsageRecords(static::$otherMeterId, price: static::$otherMeteredEventPrice)->first();
+
+ $this->assertSame($summary->aggregated_value, 20.0);
+ }
+
+
+
public function test_swap_metered_price_to_different_price()
{
$user = $this->createCustomer('swap_metered_price_to_different_price');