95 lines
3.0 KiB
PHP
95 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Support\Carbon;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* A plan line — the thing that has a name.
|
|
*
|
|
* "Team" stays Team through every price change and every resize. Orders,
|
|
* instances and contracts have referred to plans by `key` since day one, so the
|
|
* key is the one part of a family that must never move.
|
|
*/
|
|
class PlanFamily extends Model
|
|
{
|
|
use HasFactory;
|
|
use HasUuids;
|
|
|
|
protected $guarded = [];
|
|
|
|
protected static function booted(): void
|
|
{
|
|
static::updating(function (self $family) {
|
|
// Orders, instances and every subscription snapshot store this
|
|
// string. Renaming it does not rename those — it orphans them, and
|
|
// a checkout in flight for this family stops matching its own
|
|
// version. Display names are what the `name` column is for.
|
|
if ($family->isDirty('key')) {
|
|
throw new RuntimeException(
|
|
'A plan family key is permanent; existing orders and contracts refer to it by name. '.
|
|
'Change the display name instead.'
|
|
);
|
|
}
|
|
});
|
|
|
|
static::deleting(function (self $family) {
|
|
if ($family->versions()->whereNotNull('published_at')->exists()) {
|
|
throw new RuntimeException(
|
|
'A plan family with published versions cannot be deleted; customers are contracted to them. '.
|
|
'Switch sales off instead — it disappears from the shop and stays on record.'
|
|
);
|
|
}
|
|
|
|
// Only drafts left. The foreign key restricts, so they have to go
|
|
// explicitly — and nothing was ever promised on a draft.
|
|
$family->versions->each->delete();
|
|
});
|
|
}
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'tier' => 'integer',
|
|
'sales_enabled' => 'boolean',
|
|
];
|
|
}
|
|
|
|
public function uniqueIds(): array
|
|
{
|
|
return ['uuid'];
|
|
}
|
|
|
|
public function versions(): HasMany
|
|
{
|
|
return $this->hasMany(PlanVersion::class);
|
|
}
|
|
|
|
/**
|
|
* The version on sale at a given moment, or null.
|
|
*
|
|
* Resolved with sole(): two overlapping windows are a mistake we want to
|
|
* hear about, not one we want silently resolved by whichever row the
|
|
* database happened to return first.
|
|
*
|
|
* @throws \Illuminate\Database\MultipleRecordsFoundException on overlap
|
|
*/
|
|
public function versionAt(?Carbon $at = null): ?PlanVersion
|
|
{
|
|
$query = $this->versions()->available($at);
|
|
|
|
return $query->count() === 0 ? null : $query->sole();
|
|
}
|
|
|
|
/** On sale right now: not killed by the switch, and a version is running. */
|
|
public function isSellableAt(?Carbon $at = null): bool
|
|
{
|
|
return $this->sales_enabled && $this->versionAt($at) !== null;
|
|
}
|
|
}
|