getDirty()), self::FROZEN); if ($frozen !== []) { throw new RuntimeException( 'A booked module is frozen at its booked price; tried to change: '.implode(', ', $frozen). '. Cancel it and book it again at today\'s price instead.' ); } }); } protected function casts(): array { return [ 'price_cents' => 'integer', 'quantity' => 'integer', 'booked_at' => 'datetime', 'cancelled_at' => 'datetime', 'granted_at' => 'datetime', 'granted_until' => 'datetime', 'catalogue_price_cents' => 'integer', ]; } public function uniqueIds(): array { return ['uuid']; } public function subscription(): BelongsTo { return $this->belongsTo(Subscription::class); } public function order(): BelongsTo { return $this->belongsTo(Order::class); } /** The operator who gave this module away, when it was a grant. */ public function grantedBy(): BelongsTo { return $this->belongsTo(Operator::class, 'granted_by'); } public function isGranted(): bool { return $this->granted_at !== null; } public function scopeActive(Builder $query): Builder { return $query->whereNull('cancelled_at'); } /** Net per month for this booking, packs included. */ public function monthlyCents(): int { return $this->price_cents * $this->quantity; } public function isActive(): bool { return $this->cancelled_at === null; } }