*/ use HasFactory, HasUuid; protected $fillable = [ 'customer_id', 'plan', 'type', 'addon_key', 'amount_cents', 'currency', 'datacenter', 'stripe_event_id', 'status', ]; protected function casts(): array { return ['amount_cents' => 'integer']; } /** * What this order actually buys, in words. * * Lives on the model rather than in the view: the cart, the invoice list and * any future confirmation mail must not each invent their own wording for * the same row. */ public function label(): string { return match ($this->type) { 'upgrade' => __('billing.cart.upgrade', ['plan' => ucfirst($this->plan)]), 'storage' => __('billing.cart.storage', ['gb' => (int) config('provisioning.storage_addon.gb', 100)]), 'traffic' => __('billing.cart.traffic', ['gb' => (int) config('provisioning.traffic.addon.gb', 1000)]), 'addon' => __('billing.addon.'.$this->addon_key.'.name'), default => __('billing.cart.plan', ['plan' => ucfirst($this->plan)]), }; } /** * Monthly or one-off. * * Traffic is bought for the month that is running out and does not renew; * everything else changes the recurring bill. */ public function isRecurring(): bool { return $this->type !== 'traffic'; } /** * Net is what is stored; gross is what gets charged — and how much VAT that * is depends on the customer, not on a global setting. */ public function grossCents(): int { return $this->taxTreatment()->grossCents($this->amount_cents); } public function taxTreatment(): TaxTreatment { return TaxTreatment::for($this->customer); } /** Only a pending order is still the customer's to change. */ public function isRemovable(): bool { return $this->status === 'pending'; } public function customer(): BelongsTo { return $this->belongsTo(Customer::class); } public function instance(): HasOne { return $this->hasOne(Instance::class); } /** The contract this order opened, if it was the purchase that opened one. */ public function subscription(): HasOne { return $this->hasOne(Subscription::class); } public function runs(): MorphMany { return $this->morphMany(ProvisioningRun::class, 'subject'); } /** * Runner hook: a failed provisioning run marks the order failed (no * auto-refund) AND releases the reserved instance, so its quota stops * counting against host capacity. */ public function onProvisioningFailed(): void { $this->update(['status' => 'failed']); $this->instance()->update(['status' => 'failed']); } }