fix(discord): show old → new plan on plan change notification (#637)

## 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.
This commit is contained in:
Víctor Falcón 2026-07-04 17:49:09 +02:00 committed by GitHub
parent 02087abcc7
commit 3972007844
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 49 additions and 10 deletions

View File

@ -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<string, mixed> $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<string, mixed> $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);
}
/**

View File

@ -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();