Files
ladill-frontdesk/app/Services/Frontdesk/NotificationBillingService.php
T
isaaccladandCursor 8300cafc36
Deploy Ladill Frontdesk / deploy (push) Successful in 45s
Add per-customer SMS and Bird credentials to Frontdesk Integrations.
NotificationDispatcher sends via customer relay and skips Frontdesk wallet debit when tenant keys are configured.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 17:05:54 +00:00

148 lines
4.1 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
{
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 canAffordEmail(Organization $organization): bool
{
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->usesCustomerSms($organization)) {
return true;
}
$cost = $this->pricing->smsCostMinor($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->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->usesCustomerSms($organization)) {
$this->usage->recordSms($organization, 0);
return true;
}
$cost = $this->pricing->smsCostMinor($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;
}
}
}