*/ use HasFactory, HasUuid; protected $fillable = [ 'customer_id', 'plan', 'plan_version_id', 'type', 'addon_key', 'amount_cents', 'currency', 'datacenter', 'stripe_event_id', 'stripe_subscription_id', 'status', ]; protected function casts(): array { return ['amount_cents' => 'integer', 'plan_version_id' => '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); } /** The module this order booked, if it was an add-on order. */ public function subscriptionAddon(): HasOne { return $this->hasOne(SubscriptionAddon::class); } /** * A grant that costs nothing — the one case IssueInvoice must skip. * * Keyed on the linked subscription/add-on's OWN provenance, not merely on * `amount_cents === 0`: a fully discounted Stripe checkout also charges * zero and that invoice is still owed (OpenSubscription's docblock says * the same about the register). Only a grant with nothing charged, and * chosen as a full gift rather than a discount, produces no invoice. */ public function isFreeGrant(): bool { if ((int) $this->amount_cents !== 0) { return false; } return $this->subscription?->isFreeGrant() ?? $this->subscriptionAddon?->isGranted() ?? false; } 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']); } }