withdrawalOpenedAt(); $endsAt = $subscription->withdrawal_ends_at // A contract opened before the column existed still had a window; // it is derived here rather than left null so an old contract reads // as "expired" instead of "no window was ever counted". ?? $opensAt?->copy()->addDays(self::WINDOW_DAYS); // Asked of the CUSTOMER, never of the VAT number. A customer nobody has // asked is a consumer here — see Customer::isConsumer() for why the // unknown case leans this way and not the other. if ($subscription->customer?->isBusiness() === true) { return new self(false, $opensAt, $endsAt, false, __('withdrawal.refusal_business')); } if ($subscription->isWithdrawn()) { return new self(true, $opensAt, $endsAt, false, __('withdrawal.refusal_already')); } if ($endsAt === null) { // A contract with no beginning on record. Refused rather than // guessed: inventing the moment a statutory deadline started is the // one thing this class must never do. return new self(true, $opensAt, null, false, __('withdrawal.refusal_no_contract')); } return $endsAt->isFuture() ? new self(true, $opensAt, $endsAt, true, null) : new self(true, $opensAt, $endsAt, false, __('withdrawal.refusal_expired')); } /** * How many whole days of the window are left, for the sentence that says so. * * Rounded UP, because a customer told "1 Tag" on the last afternoon still * has that afternoon, and rounding down would tell them the window had shut * while it was open. */ public function daysLeft(?Carbon $at = null): int { if (! $this->open || $this->endsAt === null) { return 0; } return max(1, (int) ceil(($at ?? now())->diffInSeconds($this->endsAt, absolute: false) / 86400)); } /** * The whole days the term the customer paid for is made of. * * From the contract's own boundaries, so a month is 28, 30 or 31 days and a * year is 365 or 366. Nothing is charged against it any more — it is what * `deliveredDays()` is a share OF, in the register and on the operator's * screen. */ public static function termDays(Subscription $subscription): int { $from = $subscription->current_period_start ?? $subscription->started_at; $to = $subscription->current_period_end; if ($from === null || $to === null || ! $to->greaterThan($from)) { // Nothing sensible to divide by. One day means the first day of // service is the whole term, which charges the consumer the full // amount — so this branch is only ever reached by a contract that is // already broken, and it fails towards "we keep what we were paid" // rather than towards a division by zero. return 1; } return max(1, (int) ceil($from->diffInSeconds($to, absolute: true) / 86400)); } /** * The days of service actually delivered by the moment of withdrawal. * * A started day counts, so this is never zero: a consumer who withdraws * three hours after buying still had a running cloud for those three hours. * Capped at the term, because no more of a term can be delivered than it * holds. */ public static function deliveredDays(Subscription $subscription, ?Carbon $at = null): int { $from = $subscription->current_period_start ?? $subscription->started_at; $at ??= now(); if ($from === null || ! $at->greaterThan($from)) { return 1; } $days = (int) ceil($from->diffInSeconds($at, absolute: true) / 86400); return max(1, min($days, self::termDays($subscription))); } }