Deploy Ladill Frontdesk / deploy (push) Has been cancelled
Pro includes 5,000 email and 5,000 SMS host alerts per month with hard monthly caps; Enterprise is unlimited. Custom badge templates are Enterprise-only, and plan benefit copy matches the new limits.
205 lines
5.7 KiB
PHP
205 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Frontdesk;
|
|
|
|
use App\Models\Organization;
|
|
use App\Services\Billing\BillingClient;
|
|
use App\Services\Messaging\MessagingCredentialsService;
|
|
use Illuminate\Support\Str;
|
|
|
|
class NotificationBillingService
|
|
{
|
|
public function __construct(
|
|
protected BillingClient $billing,
|
|
protected NotificationPricingService $pricing,
|
|
protected NotificationUsageService $usage,
|
|
protected PlanService $plans,
|
|
protected MessagingCredentialsService $credentials,
|
|
) {}
|
|
|
|
public function usesCustomerEmail(Organization $organization): bool
|
|
{
|
|
return $this->credentials->forOrganization($organization)->hasValidBird();
|
|
}
|
|
|
|
public function usesCustomerSms(Organization $organization): bool
|
|
{
|
|
return $this->credentials->forOrganization($organization)->hasValidSms();
|
|
}
|
|
|
|
public function emailCostMinor(Organization $organization): int
|
|
{
|
|
// Paid plan monthly cap exhausted — do not offer overage.
|
|
if ($this->plans->emailAllowanceExhausted(
|
|
$organization,
|
|
$this->usage->emailCountThisMonth($organization),
|
|
)) {
|
|
return 0;
|
|
}
|
|
|
|
if ($this->usesCustomerEmail($organization)) {
|
|
return 0;
|
|
}
|
|
|
|
$allowance = $this->plans->freeEmailsPerMonth($organization);
|
|
if ($allowance === null || $this->usage->emailCountThisMonth($organization) < $allowance) {
|
|
return 0;
|
|
}
|
|
|
|
return $this->pricing->emailCostMinor();
|
|
}
|
|
|
|
public function smsCostMinor(Organization $organization, string $message): int
|
|
{
|
|
if ($this->plans->smsAllowanceExhausted(
|
|
$organization,
|
|
$this->usage->smsCountThisMonth($organization),
|
|
)) {
|
|
return 0;
|
|
}
|
|
|
|
if ($this->usesCustomerSms($organization)) {
|
|
return 0;
|
|
}
|
|
|
|
$allowance = $this->plans->freeSmsPerMonth($organization);
|
|
if ($allowance === null || $this->usage->smsCountThisMonth($organization) < $allowance) {
|
|
return 0;
|
|
}
|
|
|
|
return $this->pricing->smsCostMinor($message);
|
|
}
|
|
|
|
public function canAffordEmail(Organization $organization): bool
|
|
{
|
|
if ($this->plans->emailAllowanceExhausted(
|
|
$organization,
|
|
$this->usage->emailCountThisMonth($organization),
|
|
)) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->usesCustomerEmail($organization)) {
|
|
return true;
|
|
}
|
|
|
|
$cost = $this->emailCostMinor($organization);
|
|
if ($cost <= 0) {
|
|
return true;
|
|
}
|
|
|
|
return $this->canAfford($organization->owner_ref, $cost);
|
|
}
|
|
|
|
public function canAffordSms(Organization $organization, string $message): bool
|
|
{
|
|
if ($this->plans->smsAllowanceExhausted(
|
|
$organization,
|
|
$this->usage->smsCountThisMonth($organization),
|
|
)) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->usesCustomerSms($organization)) {
|
|
return true;
|
|
}
|
|
|
|
$cost = $this->smsCostMinor($organization, $message);
|
|
if ($cost <= 0) {
|
|
return true;
|
|
}
|
|
|
|
return $this->canAfford($organization->owner_ref, $cost);
|
|
}
|
|
|
|
/**
|
|
* Debit after a successful send. Returns false if the wallet could not be charged.
|
|
* Customer relay keys bill the Ladill SMS/Bird wallet directly — never double-debit Frontdesk.
|
|
*/
|
|
public function chargeEmail(Organization $organization, string $reference, string $description): bool
|
|
{
|
|
if ($this->plans->emailAllowanceExhausted(
|
|
$organization,
|
|
$this->usage->emailCountThisMonth($organization),
|
|
)) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->usesCustomerEmail($organization)) {
|
|
$this->usage->recordEmail($organization, 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
$cost = $this->emailCostMinor($organization);
|
|
if ($cost <= 0) {
|
|
$this->usage->recordEmail($organization, 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (! $this->debit($organization->owner_ref, $cost, 'email', $reference, $description)) {
|
|
return false;
|
|
}
|
|
|
|
$this->usage->recordEmail($organization, $cost);
|
|
|
|
return true;
|
|
}
|
|
|
|
public function chargeSms(Organization $organization, string $message, string $reference, string $description): bool
|
|
{
|
|
if ($this->plans->smsAllowanceExhausted(
|
|
$organization,
|
|
$this->usage->smsCountThisMonth($organization),
|
|
)) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->usesCustomerSms($organization)) {
|
|
$this->usage->recordSms($organization, 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
$cost = $this->smsCostMinor($organization, $message);
|
|
if ($cost <= 0) {
|
|
$this->usage->recordSms($organization, 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (! $this->debit($organization->owner_ref, $cost, 'sms', $reference, $description)) {
|
|
return false;
|
|
}
|
|
|
|
$this->usage->recordSms($organization, $cost);
|
|
|
|
return true;
|
|
}
|
|
|
|
private function canAfford(string $ownerRef, int $costMinor): bool
|
|
{
|
|
try {
|
|
return $this->billing->canAfford($ownerRef, $costMinor);
|
|
} catch (\Throwable) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private function debit(string $ownerRef, int $costMinor, string $channel, string $reference, string $description): bool
|
|
{
|
|
try {
|
|
return $this->billing->debit(
|
|
$ownerRef,
|
|
$costMinor,
|
|
'notification_'.$channel,
|
|
'frontdesk-notify-'.$reference.'-'.Str::uuid(),
|
|
$description,
|
|
);
|
|
} catch (\Throwable) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|