monthlyCostGhs(); $minor = (int) round($ghs * 100); return max($minor, 1); } /** Charge the first monthly period when a transfer is created. */ public function chargeInitialPeriod(Transfer $transfer, User $user): void { $amountMinor = $this->amountMinorFor($transfer); if (! $this->billing->canAfford($user->public_id, $amountMinor)) { throw new RuntimeException('Insufficient wallet balance for the first month of storage. Top up your Ladill wallet and try again.'); } $periodEnd = $this->nextPeriodEnd(now()); $reference = $this->billingReference($transfer, $periodEnd); if (! $this->billing->debit( $user->public_id, $amountMinor, (string) config('billing.service', 'transfer'), 'transfer_storage', $reference, $transfer->id, sprintf('Transfer storage: %s (%s)', $transfer->title, $transfer->storageGb().' GB'), )) { throw new RuntimeException('Could not debit your wallet for storage. Top up and try again.'); } $this->markPaidThrough($transfer, $periodEnd); } /** Attempt to renew a transfer for another month. */ public function chargeRenewal(Transfer $transfer): bool { $transfer->loadMissing('user'); $user = $transfer->user; if ($user === null || $user->public_id === null) { return false; } $amountMinor = $this->amountMinorFor($transfer); $base = $transfer->paid_until instanceof CarbonInterface ? $transfer->paid_until->copy() : now(); $periodEnd = $this->nextPeriodEnd($base); $reference = $this->billingReference($transfer, $periodEnd); if (! $this->billing->canAfford($user->public_id, $amountMinor)) { return false; } if (! $this->billing->debit( $user->public_id, $amountMinor, (string) config('billing.service', 'transfer'), 'transfer_storage_renewal', $reference, $transfer->id, sprintf('Transfer storage renewal: %s', $transfer->title), )) { return false; } $this->markPaidThrough($transfer, $periodEnd); return true; } public function enterGracePeriod(Transfer $transfer): void { $graceEndsAt = now()->addDays($this->gracePeriodDays()); $transfer->update([ 'status' => Transfer::STATUS_GRACE, 'grace_ends_at' => $graceEndsAt, 'expires_at' => $graceEndsAt, ]); $transfer = $transfer->fresh(['user', 'files', 'qrCode']); app(TransferRecipientMailService::class)->notifyRecipient($transfer, 'grace'); app(TransferOwnerMailService::class)->notifyGraceEntered($transfer); } public function processDueRenewals(): array { $renewed = 0; $graced = 0; $deleted = 0; $due = Transfer::query() ->whereIn('status', [Transfer::STATUS_ACTIVE, Transfer::STATUS_GRACE]) ->whereNotNull('paid_until') ->where('paid_until', '<=', now()) ->with('user') ->get(); foreach ($due as $transfer) { if ($this->chargeRenewal($transfer)) { $renewed++; continue; } if ($transfer->status !== Transfer::STATUS_GRACE) { $this->enterGracePeriod($transfer); $graced++; } } $expired = Transfer::query() ->where('status', Transfer::STATUS_GRACE) ->whereNotNull('grace_ends_at') ->where('grace_ends_at', '<=', now()) ->get(); foreach ($expired as $transfer) { $this->transfers->delete($transfer); $deleted++; } return compact('renewed', 'graced', 'deleted'); } private function markPaidThrough(Transfer $transfer, CarbonInterface $periodEnd): void { $transfer->update([ 'status' => Transfer::STATUS_ACTIVE, 'paid_until' => $periodEnd, 'expires_at' => $periodEnd, 'grace_ends_at' => null, 'last_billed_at' => now(), ]); } private function nextPeriodEnd(CarbonInterface $from): CarbonInterface { return Carbon::instance($from)->addMonthNoOverflow(); } private function billingReference(Transfer $transfer, CarbonInterface $periodEnd): string { return sprintf('transfer:%d:through:%s', $transfer->id, $periodEnd->format('Y-m-d')); } }