From 73c2d834649a8a00a87ed256cd09ea86e57d4fb5 Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Sat, 12 Sep 2026 10:22:50 -0400 Subject: [PATCH] Expand admin-sales-summary with full attributable revenue. Include Ultra Cashier subscriptions plus plugin and first-party product sales (sales_view), with breakdown, by-product rollups, comps, all_time lookback, and notes on price_paid renewal limitations. --- app/Mcp/Servers/AdminNativePhpServer.php | 2 +- app/Mcp/Tools/Admin/AdminSalesSummary.php | 405 ++++++++++++++++++-- tests/Feature/Mcp/AdminSalesSummaryTest.php | 230 +++++++++++ 3 files changed, 604 insertions(+), 33 deletions(-) create mode 100644 tests/Feature/Mcp/AdminSalesSummaryTest.php diff --git a/app/Mcp/Servers/AdminNativePhpServer.php b/app/Mcp/Servers/AdminNativePhpServer.php index e47f767c..a1fcdc7e 100644 --- a/app/Mcp/Servers/AdminNativePhpServer.php +++ b/app/Mcp/Servers/AdminNativePhpServer.php @@ -43,7 +43,7 @@ class AdminNativePhpServer extends Server 5. admin-get-blog-post / admin-list-blog-posts — inspect drafts and published posts. 6. admin-list-signups / admin-search-users / admin-get-user — user support lookups. 7. admin-list-companies / admin-get-company — email-domain company rollups. - 8. admin-search-plugins / admin-sales-summary — marketplace/plugin ops (no secrets). + 8. admin-search-plugins / admin-sales-summary — marketplace ops + attributable revenue (plugins, products, Ultra Cashier; no secrets). 9. admin-search-support-tickets / admin-get-support-ticket — support summaries. MARKDOWN; diff --git a/app/Mcp/Tools/Admin/AdminSalesSummary.php b/app/Mcp/Tools/Admin/AdminSalesSummary.php index f40526b6..22f2c8b5 100644 --- a/app/Mcp/Tools/Admin/AdminSalesSummary.php +++ b/app/Mcp/Tools/Admin/AdminSalesSummary.php @@ -6,9 +6,15 @@ use App\Models\License; use App\Models\Plugin; use App\Models\PluginLicense; +use App\Models\ProductLicense; +use App\Models\Sale; +use Carbon\CarbonInterface; use Illuminate\Contracts\JsonSchema\JsonSchema; +use Illuminate\Database\Eloquent\Builder; use Illuminate\JsonSchema\Types\Type; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; +use Laravel\Cashier\Subscription; use Laravel\Mcp\Request; use Laravel\Mcp\Response; use Laravel\Mcp\Server\Attributes\Description; @@ -17,12 +23,14 @@ use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly; #[Name('admin-sales-summary')] -#[Description('Read-only sales/license summaries: plugin license revenue and NativePHP license counts. Never returns raw license keys or Stripe secrets.')] +#[Description('Read-only attributable inbound revenue: Ultra (Cashier) subscriptions + plugin + first-party product sales from sales_view, plus Anystack NativePHP license counts. Never returns raw license keys or Stripe secrets.')] #[IsReadOnly] class AdminSalesSummary extends Tool { use RequiresAdmin; + private const BY_PRODUCT_LIMIT = 50; + public function handle(Request $request): Response { if ($denied = $this->ensureAdmin($request)) { @@ -30,26 +38,346 @@ public function handle(Request $request): Response } $validated = $request->validate([ - 'days' => ['nullable', 'integer', 'min:1', 'max:365'], + 'days' => ['nullable', 'integer', 'min:1', 'max:3650'], + 'all_time' => ['nullable', 'boolean'], ]); - $days = (int) ($validated['days'] ?? 30); - $since = now()->subDays($days); + $allTime = (bool) ($validated['all_time'] ?? false); + $days = $allTime ? null : (int) ($validated['days'] ?? 30); + $since = $allTime ? null : now()->subDays($days); - $pluginRevenue = PluginLicense::query() - ->where('purchased_at', '>=', $since) - ->selectRaw('currency, COUNT(*) as licenses_count, COALESCE(SUM(price_paid), 0) as revenue_cents') - ->groupBy('currency') + $pluginPaid = $this->aggregatePluginLicenses($since, paidOnly: true); + $productPaid = $this->aggregateProductLicenses($since, paidOnly: true); + $pluginCompedCount = $this->countPluginComps($since); + $productCompedCount = $this->countProductComps($since); + + $ultra = $this->ultraSummary($since); + + $breakdown = $this->buildBreakdown($pluginPaid, $productPaid, $ultra); + $totalRevenueCents = $this->sumPreferredCurrency($breakdown); + + $pluginRevenue = $pluginPaid->map(fn (array $row): array => [ + 'currency' => $row['currency'], + 'licenses_count' => $row['sales_count'], + 'revenue_cents' => $row['revenue_cents'], + ])->values()->all(); + + $topPlugins = $this->topPlugins($since); + $byProduct = $this->byProduct($since); + + $nativeLicensesQuery = License::query(); + if ($since !== null) { + $nativeLicensesQuery->where('created_at', '>=', $since); + } + + $nativeLicenses = $nativeLicensesQuery + ->selectRaw('policy_name, COUNT(*) as count') + ->groupBy('policy_name') + ->orderByDesc('count') ->get() ->map(fn ($row): array => [ - 'currency' => $row->currency, - 'licenses_count' => (int) $row->licenses_count, - 'revenue_cents' => (int) $row->revenue_cents, + 'policy_name' => $row->policy_name, + 'count' => (int) $row->count, ]) ->all(); - $topPluginRows = PluginLicense::query() - ->where('purchased_at', '>=', $since) + return Response::text($this->toJson([ + 'days' => $days, + 'all_time' => $allTime, + 'since' => $since?->toIso8601String(), + 'total_revenue_cents' => $totalRevenueCents, + 'breakdown' => $breakdown, + 'by_product' => $byProduct, + 'plugin_license_revenue' => $pluginRevenue, + 'top_plugins' => $topPlugins, + 'ultra' => $ultra, + 'nativephp_licenses_created' => $nativeLicenses, + 'active_nativephp_licenses' => License::query()->whereActive()->count(), + 'comped' => [ + 'plugin_count' => $pluginCompedCount, + 'product_count' => $productCompedCount, + 'ultra_active_count' => $ultra['active_comped_count'], + ], + 'note' => implode(' ', [ + 'Amounts are in cents (prefer USD when summing total_revenue_cents across currencies).', + 'sales_view unions plugin_licenses + product_licenses (one-time sales); paid revenue excludes is_comped / $0 rows (comps counted separately).', + 'Ultra is Laravel Cashier subscriptions on Max/Ultra Stripe prices (plan key max, display name Ultra) — not an Anystack policy.', + 'Ultra revenue_cents_in_window sums subscriptions.price_paid for Ultra subs created in the window with price_paid > 0; HandleInvoicePaidJob overwrites price_paid with the latest invoice total (not cumulative), so renewals do not create new rows and window revenue under-counts renewals — same limitation as Filament SubscriberIncomeChart.', + 'License keys and Stripe secrets are intentionally omitted.', + ]), + ])); + } + + /** + * @return list + */ + protected function ultraPayingPriceIds(): array + { + return array_values(array_filter([ + config('subscriptions.plans.max.stripe_price_id'), + config('subscriptions.plans.max.stripe_price_id_monthly'), + config('subscriptions.plans.max.stripe_price_id_eap'), + config('subscriptions.plans.max.stripe_price_id_discounted'), + ])); + } + + protected function ultraCompedPriceId(): ?string + { + $id = config('subscriptions.plans.max.stripe_price_id_comped'); + + return is_string($id) && $id !== '' ? $id : null; + } + + /** + * @return list + */ + protected function allUltraPriceIds(): array + { + return array_values(array_filter([ + ...$this->ultraPayingPriceIds(), + $this->ultraCompedPriceId(), + ])); + } + + /** + * @param Builder $query + * @param list $priceIds + * @return Builder + */ + protected function whereHasUltraPrice(Builder $query, array $priceIds): Builder + { + if ($priceIds === []) { + return $query->whereRaw('1 = 0'); + } + + return $query->where(function (Builder $inner) use ($priceIds): void { + $inner->whereIn('stripe_price', $priceIds) + ->orWhereHas('items', fn (Builder $items) => $items->whereIn('stripe_price', $priceIds)); + }); + } + + /** + * @return array{ + * active_count: int, + * active_comped_count: int, + * created_in_window: int, + * paid_created_in_window: int, + * revenue_cents_in_window: int, + * currency: string, + * caveat: string + * } + */ + protected function ultraSummary(?CarbonInterface $since): array + { + $payingIds = $this->ultraPayingPriceIds(); + $compedId = $this->ultraCompedPriceId(); + $allIds = $this->allUltraPriceIds(); + + $activePaying = Subscription::query() + ->active() + ->where(function (Builder $q): void { + $q->where('is_comped', false)->orWhereNull('is_comped'); + }); + $this->whereHasUltraPrice($activePaying, $payingIds); + + $activeCompedCount = 0; + if ($compedId) { + $activeComped = Subscription::query()->active(); + $this->whereHasUltraPrice($activeComped, [$compedId]); + $activeCompedCount = $activeComped->count(); + } + + $createdQuery = Subscription::query(); + $this->whereHasUltraPrice($createdQuery, $allIds); + if ($since !== null) { + $createdQuery->where('created_at', '>=', $since); + } + $createdInWindow = (clone $createdQuery)->count(); + + $revenueQuery = Subscription::query() + ->whereNotNull('price_paid') + ->where('price_paid', '>', 0) + ->where(function (Builder $q): void { + $q->where('is_comped', false)->orWhereNull('is_comped'); + }); + $this->whereHasUltraPrice($revenueQuery, $payingIds); + if ($since !== null) { + $revenueQuery->where('created_at', '>=', $since); + } + $revenueCents = (int) (clone $revenueQuery)->sum('price_paid'); + $paidSalesCount = (clone $revenueQuery)->count(); + + return [ + 'active_count' => $activePaying->count(), + 'active_comped_count' => $activeCompedCount, + 'created_in_window' => $createdInWindow, + 'paid_created_in_window' => $paidSalesCount, + 'revenue_cents_in_window' => $revenueCents, + 'currency' => 'USD', + 'caveat' => 'price_paid is overwritten on each paid invoice (not cumulative); renewals do not add rows. Window revenue only includes Ultra/Max subs created in-range with price_paid > 0.', + ]; + } + + /** + * @return Collection + */ + protected function aggregatePluginLicenses(?CarbonInterface $since, bool $paidOnly): Collection + { + $query = PluginLicense::query(); + if ($since !== null) { + $query->where('purchased_at', '>=', $since); + } + if ($paidOnly) { + $query->where('is_grandfathered', false)->where('price_paid', '>', 0); + } + + return $query + ->selectRaw('currency, COUNT(*) as sales_count, COALESCE(SUM(price_paid), 0) as revenue_cents') + ->groupBy('currency') + ->get() + ->map(fn ($row): array => [ + 'currency' => $row->currency ?: 'USD', + 'sales_count' => (int) $row->sales_count, + 'revenue_cents' => (int) $row->revenue_cents, + ]); + } + + /** + * @return Collection + */ + protected function aggregateProductLicenses(?CarbonInterface $since, bool $paidOnly): Collection + { + $query = ProductLicense::query(); + if ($since !== null) { + $query->where('purchased_at', '>=', $since); + } + if ($paidOnly) { + $query->where('is_comped', false)->where('price_paid', '>', 0); + } + + return $query + ->selectRaw('currency, COUNT(*) as sales_count, COALESCE(SUM(price_paid), 0) as revenue_cents') + ->groupBy('currency') + ->get() + ->map(fn ($row): array => [ + 'currency' => $row->currency ?: 'USD', + 'sales_count' => (int) $row->sales_count, + 'revenue_cents' => (int) $row->revenue_cents, + ]); + } + + protected function countPluginComps(?CarbonInterface $since): int + { + $query = PluginLicense::query()->where(function (Builder $q): void { + $q->where('is_grandfathered', true)->orWhere('price_paid', '<=', 0); + }); + if ($since !== null) { + $query->where('purchased_at', '>=', $since); + } + + return $query->count(); + } + + protected function countProductComps(?CarbonInterface $since): int + { + $query = ProductLicense::query()->where(function (Builder $q): void { + $q->where('is_comped', true)->orWhere('price_paid', '<=', 0); + }); + if ($since !== null) { + $query->where('purchased_at', '>=', $since); + } + + return $query->count(); + } + + /** + * @param Collection $pluginPaid + * @param Collection $productPaid + * @param array{revenue_cents_in_window: int, currency: string, caveat: string, created_in_window: int, paid_created_in_window: int} $ultra + * @return list + */ + protected function buildBreakdown(Collection $pluginPaid, Collection $productPaid, array $ultra): array + { + $rows = []; + + foreach ($pluginPaid as $row) { + $rows[] = [ + 'source' => 'plugins', + 'revenue_cents' => $row['revenue_cents'], + 'sales_count' => $row['sales_count'], + 'currency' => $row['currency'], + ]; + } + + if ($pluginPaid->isEmpty()) { + $rows[] = [ + 'source' => 'plugins', + 'revenue_cents' => 0, + 'sales_count' => 0, + 'currency' => 'USD', + ]; + } + + foreach ($productPaid as $row) { + $rows[] = [ + 'source' => 'products', + 'revenue_cents' => $row['revenue_cents'], + 'sales_count' => $row['sales_count'], + 'currency' => $row['currency'], + ]; + } + + if ($productPaid->isEmpty()) { + $rows[] = [ + 'source' => 'products', + 'revenue_cents' => 0, + 'sales_count' => 0, + 'currency' => 'USD', + ]; + } + + $rows[] = [ + 'source' => 'ultra_subscriptions', + 'revenue_cents' => $ultra['revenue_cents_in_window'], + 'sales_count' => $ultra['paid_created_in_window'], + 'currency' => $ultra['currency'], + 'caveat' => $ultra['caveat'], + ]; + + return $rows; + } + + /** + * @param list $breakdown + */ + protected function sumPreferredCurrency(array $breakdown): int + { + $byCurrency = collect($breakdown)->groupBy('currency'); + + if ($byCurrency->has('USD')) { + return (int) $byCurrency->get('USD')->sum('revenue_cents'); + } + + if ($byCurrency->count() === 1) { + return (int) $byCurrency->first()->sum('revenue_cents'); + } + + return (int) collect($breakdown)->sum('revenue_cents'); + } + + /** + * @return list + */ + protected function topPlugins(?CarbonInterface $since): array + { + $query = PluginLicense::query() + ->where('is_grandfathered', false) + ->where('price_paid', '>', 0); + if ($since !== null) { + $query->where('purchased_at', '>=', $since); + } + + $topPluginRows = $query ->select('plugin_id', DB::raw('COUNT(*) as licenses_count'), DB::raw('COALESCE(SUM(price_paid), 0) as revenue_cents')) ->groupBy('plugin_id') ->orderByDesc('licenses_count') @@ -61,7 +389,7 @@ public function handle(Request $request): Response ->get(['id', 'name', 'status']) ->keyBy('id'); - $topPlugins = $topPluginRows->map(function ($row) use ($plugins): array { + return $topPluginRows->map(function ($row) use ($plugins): array { $plugin = $plugins->get($row->plugin_id); return [ @@ -72,28 +400,40 @@ public function handle(Request $request): Response 'revenue_cents' => (int) $row->revenue_cents, ]; })->all(); + } - $nativeLicenses = License::query() - ->where('created_at', '>=', $since) - ->selectRaw('policy_name, COUNT(*) as count') - ->groupBy('policy_name') - ->orderByDesc('count') + /** + * @return list + */ + protected function byProduct(?CarbonInterface $since): array + { + $query = Sale::query() + ->where(function (Builder $q): void { + $q->where('is_comped', false)->orWhereNull('is_comped'); + }) + ->where('price_paid', '>', 0); + + if ($since !== null) { + $query->where('purchased_at', '>=', $since); + } + + // sales_view ids are pl_{id} (plugins) or pr_{id} (products) + $sourceExpression = "CASE WHEN id LIKE 'pl_%' THEN 'plugins' ELSE 'products' END"; + + return $query + ->selectRaw("product_name, currency, {$sourceExpression} as source, COUNT(*) as sales_count, COALESCE(SUM(price_paid), 0) as revenue_cents") + ->groupByRaw("product_name, currency, {$sourceExpression}") + ->orderByDesc('revenue_cents') + ->limit(self::BY_PRODUCT_LIMIT) ->get() ->map(fn ($row): array => [ - 'policy_name' => $row->policy_name, - 'count' => (int) $row->count, + 'product_name' => $row->product_name, + 'source' => $row->source, + 'revenue_cents' => (int) $row->revenue_cents, + 'sales_count' => (int) $row->sales_count, + 'currency' => $row->currency ?: 'USD', ]) ->all(); - - return Response::text($this->toJson([ - 'days' => $days, - 'since' => $since->toIso8601String(), - 'plugin_license_revenue' => $pluginRevenue, - 'top_plugins' => $topPlugins, - 'nativephp_licenses_created' => $nativeLicenses, - 'active_nativephp_licenses' => License::query()->whereActive()->count(), - 'note' => 'Amounts are in cents. License keys and Stripe secrets are intentionally omitted.', - ])); } /** @@ -102,7 +442,8 @@ public function handle(Request $request): Response public function schema(JsonSchema $schema): array { return [ - 'days' => $schema->integer()->description('Lookback window in days (default 30, max 365).'), + 'days' => $schema->integer()->description('Lookback window in days (default 30, max 3650). Ignored when all_time is true.'), + 'all_time' => $schema->boolean()->description('When true, ignore days/since and include all attributable revenue.'), ]; } } diff --git a/tests/Feature/Mcp/AdminSalesSummaryTest.php b/tests/Feature/Mcp/AdminSalesSummaryTest.php new file mode 100644 index 00000000..af8b7494 --- /dev/null +++ b/tests/Feature/Mcp/AdminSalesSummaryTest.php @@ -0,0 +1,230 @@ +configureMcpOAuthKeys(); + $this->admin = User::factory()->create(['email' => 'admin-sales@nativephp.com']); + config([ + 'filament.users' => [$this->admin->email], + 'subscriptions.plans.max.stripe_price_id_comped' => self::COMPED_ULTRA_PRICE_ID, + 'subscriptions.plans.max.stripe_price_id_monthly' => 'price_max_monthly', + 'subscriptions.plans.max.stripe_price_id_discounted' => 'price_max_discounted', + ]); + } + + /** + * @return array + */ + private function salesSummary(array $arguments = []): array + { + $tokens = $this->issueMcpOAuthTokens($this->admin); + $response = $this->callMcpTool($tokens['access_token'], 'admin-sales-summary', $arguments)->assertOk(); + $this->assertFalse($response->json('result.isError'), (string) data_get($response->json(), 'result.content.0.text')); + + return json_decode((string) data_get($response->json(), 'result.content.0.text'), true); + } + + private function createPaidUltraSubscription(User $user, int $pricePaid, ?\DateTimeInterface $createdAt = null): Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + $priceId = SubscriptionPlan::Max->stripePriceId(); + + $subscription = Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => $priceId, + 'is_comped' => false, + 'price_paid' => $pricePaid, + 'created_at' => $createdAt ?? now(), + 'updated_at' => $createdAt ?? now(), + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => $priceId, + 'quantity' => 1, + ]); + + return $subscription; + } + + private function createCompedUltraSubscription(User $user): Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + + $subscription = Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => self::COMPED_ULTRA_PRICE_ID, + 'is_comped' => false, + 'price_paid' => 0, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => self::COMPED_ULTRA_PRICE_ID, + 'quantity' => 1, + ]); + + return $subscription; + } + + public function test_sales_summary_breakdown_includes_plugins_products_and_ultra(): void + { + $buyer = User::factory()->create(); + $plugin = Plugin::factory()->approved()->create(['name' => 'acme/camera']); + $product = Product::factory()->create(['name' => 'Plugin Dev Kit']); + + PluginLicense::factory()->create([ + 'user_id' => $buyer->id, + 'plugin_id' => $plugin->id, + 'price_paid' => 2500, + 'currency' => 'USD', + 'is_grandfathered' => false, + 'purchased_at' => now()->subDays(2), + ]); + PluginLicense::factory()->create([ + 'user_id' => $buyer->id, + 'plugin_id' => $plugin->id, + 'price_paid' => 0, + 'currency' => 'USD', + 'is_grandfathered' => true, + 'purchased_at' => now()->subDays(1), + ]); + + ProductLicense::factory()->create([ + 'user_id' => $buyer->id, + 'product_id' => $product->id, + 'price_paid' => 9900, + 'currency' => 'USD', + 'is_comped' => false, + 'purchased_at' => now()->subDays(3), + ]); + ProductLicense::factory()->create([ + 'user_id' => User::factory(), + 'product_id' => $product->id, + 'price_paid' => 0, + 'currency' => 'USD', + 'is_comped' => true, + 'purchased_at' => now()->subDay(), + ]); + + $this->createPaidUltraSubscription($buyer, 35000, now()->subDays(5)); + $this->createCompedUltraSubscription(User::factory()->create()); + $this->createPaidUltraSubscription(User::factory()->create(), 35000, now()->subDays(400)); + + License::factory()->withoutSubscriptionItem()->active()->mini()->create([ + 'user_id' => $buyer->id, + 'created_at' => now()->subDays(4), + ]); + + $payload = $this->salesSummary(['days' => 30]); + + $this->assertSame(30, $payload['days']); + $this->assertFalse($payload['all_time']); + $this->assertNotNull($payload['since']); + $this->assertSame(2500 + 9900 + 35000, $payload['total_revenue_cents']); + + $bySource = collect($payload['breakdown'])->keyBy('source'); + $this->assertSame(2500, $bySource['plugins']['revenue_cents']); + $this->assertSame(1, $bySource['plugins']['sales_count']); + $this->assertSame(9900, $bySource['products']['revenue_cents']); + $this->assertSame(1, $bySource['products']['sales_count']); + $this->assertSame(35000, $bySource['ultra_subscriptions']['revenue_cents']); + $this->assertSame(1, $bySource['ultra_subscriptions']['sales_count']); + $this->assertArrayHasKey('caveat', $bySource['ultra_subscriptions']); + + $this->assertSame(2, $payload['ultra']['active_count']); // recent + old paid Ultra still active + $this->assertSame(1, $payload['ultra']['active_comped_count']); + $this->assertSame(2, $payload['ultra']['created_in_window']); // paid + comped in window; old paid excluded + $this->assertSame(35000, $payload['ultra']['revenue_cents_in_window']); + + $this->assertSame(1, $payload['comped']['plugin_count']); + $this->assertSame(1, $payload['comped']['product_count']); + + $this->assertSame(2500, $payload['plugin_license_revenue'][0]['revenue_cents']); + $this->assertSame('acme/camera', $payload['top_plugins'][0]['plugin']); + + $byProductNames = collect($payload['by_product'])->pluck('product_name'); + $this->assertTrue($byProductNames->contains('acme/camera')); + $this->assertTrue($byProductNames->contains('Plugin Dev Kit')); + + $this->assertNotEmpty($payload['nativephp_licenses_created']); + $this->assertGreaterThanOrEqual(1, $payload['active_nativephp_licenses']); + $this->assertStringContainsString('renewals', $payload['note']); + $this->assertStringContainsString('sales_view', $payload['note']); + $this->assertStringContainsString('Cashier', $payload['note']); + } + + public function test_all_time_includes_old_ultra_and_sales(): void + { + $buyer = User::factory()->create(); + PluginLicense::factory()->create([ + 'user_id' => $buyer->id, + 'price_paid' => 1000, + 'currency' => 'USD', + 'purchased_at' => now()->subYears(3), + ]); + $this->createPaidUltraSubscription($buyer, 35000, now()->subYears(2)); + + $payload = $this->salesSummary(['all_time' => true]); + + $this->assertTrue($payload['all_time']); + $this->assertNull($payload['days']); + $this->assertNull($payload['since']); + $this->assertSame(1000 + 35000, $payload['total_revenue_cents']); + $this->assertSame(35000, $payload['ultra']['revenue_cents_in_window']); + } + + public function test_days_window_can_exceed_one_year(): void + { + $buyer = User::factory()->create(); + PluginLicense::factory()->create([ + 'user_id' => $buyer->id, + 'price_paid' => 1500, + 'currency' => 'USD', + 'purchased_at' => now()->subDays(500), + ]); + PluginLicense::factory()->create([ + 'user_id' => $buyer->id, + 'price_paid' => 999, + 'currency' => 'USD', + 'purchased_at' => now()->subDays(800), + ]); + + $payload = $this->salesSummary(['days' => 600]); + + $this->assertSame(600, $payload['days']); + $this->assertSame(1500, $payload['total_revenue_cents']); + } +}