From 39720078447f9b17dacbfd15e9986f978b87b56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Sat, 4 Jul 2026 17:49:09 +0200 Subject: [PATCH] =?UTF-8?q?fix(discord):=20show=20old=20=E2=86=92=20new=20?= =?UTF-8?q?plan=20on=20plan=20change=20notification=20(#637)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The Discord **Plan changed** notification only showed the *new* plan value in the `Changed` field, so it was impossible to tell what actually changed: ``` Changed Plan: €3.99 / month ``` Was it a price change? An interval change? From which plan? No way to know. ## Fix Compute the previous plan from Stripe's `previous_attributes` and render it as `old → new`, matching the existing status-change behaviour: ``` Changed Plan: €9.99 / month → €3.99 / month ``` `planLabel()` was generalised to read both the modern `items.data[].price` shape and the legacy top-level `plan` shape Stripe still includes in the diff, so the same helper works on both the current object and the `previous_attributes` diff. Falls back to the old `Plan: X` output when no previous value is detectable. ## Test Added a feature test asserting the `old → new` output for a `customer.subscription.updated` plan change. All 11 tests in the file pass. --- app/Listeners/PostStripeEventToDiscord.php | 34 +++++++++++++------ .../Feature/PostStripeEventToDiscordTest.php | 25 ++++++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/app/Listeners/PostStripeEventToDiscord.php b/app/Listeners/PostStripeEventToDiscord.php index a33d3272..a8b60fbd 100644 --- a/app/Listeners/PostStripeEventToDiscord.php +++ b/app/Listeners/PostStripeEventToDiscord.php @@ -206,34 +206,48 @@ class PostStripeEventToDiscord implements ShouldQueue } if (array_key_exists('items', $previous) || array_key_exists('plan', $previous)) { - $lines[] = 'Plan: '.($this->planLabel($object) ?? 'updated'); + $new = $this->planLabel($object) ?? 'updated'; + $old = $this->planLabel($previous); + $lines[] = ($old !== null && $old !== $new) + ? sprintf('Plan: %s → %s', $old, $new) + : 'Plan: '.$new; } return $lines === [] ? null : implode("\n", $lines); } /** - * @param array $object + * Build a "€3.99 / month" label from either a subscription object, or a + * previous_attributes diff. Handles both the modern `items.data[].price` + * shape and the legacy top-level `plan` shape Stripe still includes. + * + * @param array $source */ - private function planLabel(array $object): ?string + private function planLabel(array $source): ?string { - $price = $object['items']['data'][0]['price'] ?? null; + $price = $source['items']['data'][0]['price'] ?? $source['plan'] ?? null; - if (! is_array($price) || ! isset($price['unit_amount'])) { + if (! is_array($price)) { return null; } - $amount = $this->money((int) $price['unit_amount'], (string) ($price['currency'] ?? 'usd')); - $recurring = $price['recurring'] ?? null; + $amount = $price['unit_amount'] ?? $price['amount'] ?? null; - if (! is_array($recurring) || ! isset($recurring['interval'])) { - return $amount; + if ($amount === null) { + return null; + } + + $label = $this->money((int) $amount, (string) ($price['currency'] ?? 'usd')); + $recurring = is_array($price['recurring'] ?? null) ? $price['recurring'] : $price; + + if (! isset($recurring['interval'])) { + return $label; } $count = (int) ($recurring['interval_count'] ?? 1); $interval = $count > 1 ? sprintf('%d %ss', $count, $recurring['interval']) : (string) $recurring['interval']; - return sprintf('%s / %s', $amount, $interval); + return sprintf('%s / %s', $label, $interval); } /** diff --git a/tests/Feature/PostStripeEventToDiscordTest.php b/tests/Feature/PostStripeEventToDiscordTest.php index 859fa063..4301e531 100644 --- a/tests/Feature/PostStripeEventToDiscordTest.php +++ b/tests/Feature/PostStripeEventToDiscordTest.php @@ -159,6 +159,31 @@ test('reports the status change on a meaningful update', function () { ->contains(fn ($field) => $field['name'] === 'Changed' && str_contains($field['value'], 'trialing → active'))); }); +test('reports the old and new plan on a plan change', function () { + Http::fake(); + + $price = fn (int $amount) => [ + 'items' => ['data' => [['price' => [ + 'unit_amount' => $amount, + 'currency' => 'eur', + 'recurring' => ['interval' => 'month', 'interval_count' => 1], + ]]]], + ]; + + handleStripeWebhook([ + 'id' => 'evt_plan_change', + 'type' => 'customer.subscription.updated', + 'data' => [ + 'object' => ['id' => 'sub_1', 'status' => 'active', 'customer' => 'cus_1'] + $price(399), + 'previous_attributes' => $price(999), + ], + ]); + + Http::assertSent(fn ($request) => str_contains($request['embeds'][0]['title'], 'Plan changed') + && collect($request['embeds'][0]['fields']) + ->contains(fn ($field) => $field['name'] === 'Changed' && str_contains($field['value'], '€9.99 / month → €3.99 / month'))); +}); + test('ignores trivial subscription updates', function () { Http::fake();