token())->acceptJson()->timeout(10)->get($this->base().$path, $query); $res->throw(); return (array) $res->json(); } public function balanceMinor(string $publicId): int { return (int) ($this->get('/balance', ['user' => $publicId])['balance_minor'] ?? 0); } public function canAfford(string $publicId, int $amountMinor): bool { return (bool) ($this->get('/can-afford', ['user' => $publicId, 'amount_minor' => $amountMinor])['affordable'] ?? false); } public function serviceLedger(string $publicId, string $service): array { return $this->get('/service-ledger', ['user' => $publicId, 'service' => $service]); } /** * Debit the wallet. Returns true on success, false on insufficient balance * (HTTP 402). Idempotent by $reference. */ public function debit(string $publicId, int $amountMinor, string $service, string $source, string $reference, ?int $serviceId = null, ?string $description = null): bool { $res = Http::withToken($this->token())->acceptJson()->timeout(10)->post($this->base().'/debit', array_filter([ 'user' => $publicId, 'amount_minor' => $amountMinor, 'service' => $service, 'source' => $source, 'reference' => $reference, 'service_id' => $serviceId, 'description' => $description, ], static fn ($v) => $v !== null)); if ($res->status() === 402) { return false; } $res->throw(); return true; } public function credit(string $publicId, int $amountMinor, string $service, string $source, string $reference, ?int $serviceId = null, ?string $description = null): array { $res = Http::withToken($this->token())->acceptJson()->timeout(10)->post($this->base().'/credit', array_filter([ 'user' => $publicId, 'amount_minor' => $amountMinor, 'service' => $service, 'source' => $source, 'reference' => $reference, 'service_id' => $serviceId, 'description' => $description, ], static fn ($v) => $v !== null)); $res->throw(); return (array) $res->json(); } public function configured(): bool { return $this->base() !== '' && $this->token() !== ''; } /** * @return array{ok: bool, insufficient: bool, balance_minor: ?int, error: ?string} */ public function charge(User $user, int $amountMinor, string $reference, string $description): array { if (! $this->configured()) { return ['ok' => false, 'insufficient' => false, 'balance_minor' => null, 'error' => 'Billing is not configured.']; } try { $res = Http::withToken($this->token()) ->acceptJson()->timeout(20) ->post($this->base().'/debit', [ 'user' => $user->public_id, 'amount_minor' => $amountMinor, 'service' => (string) config('billing.service', 'events'), 'source' => 'subscription', 'reference' => $reference, 'description' => $description, ]); } catch (\Throwable $e) { Log::warning('Events BillingClient: charge failed', ['user' => $user->public_id, 'error' => $e->getMessage()]); return ['ok' => false, 'insufficient' => false, 'balance_minor' => null, 'error' => 'Could not reach the billing service.']; } if ($res->status() === 402) { return ['ok' => false, 'insufficient' => true, 'balance_minor' => (int) $res->json('balance_minor', 0), 'error' => 'Insufficient wallet balance.']; } if ($res->failed()) { return ['ok' => false, 'insufficient' => false, 'balance_minor' => null, 'error' => 'Billing error ('.$res->status().').']; } return ['ok' => true, 'insufficient' => false, 'balance_minor' => null, 'error' => null]; } /** * @param array $metadata * @return array{checkout_url: string, reference: string} */ public function initiatePlanCheckout( User $user, string $plan, int $months, int $amountMinor, string $returnUrl, array $metadata = [], ): array { $res = Http::withToken($this->token()) ->acceptJson()->timeout(15) ->post($this->base().'/plan-checkout', [ 'user' => $user->public_id, 'app' => (string) config('billing.service', 'events'), 'plan' => $plan, 'months' => $months, 'amount_minor' => $amountMinor, 'return_url' => $returnUrl, 'metadata' => $metadata, ]); $res->throw(); return [ 'checkout_url' => (string) $res->json('checkout_url'), 'reference' => (string) $res->json('reference'), ]; } /** @return array */ public function verifyPlanCheckout(string $reference): array { $res = Http::withToken($this->token()) ->acceptJson()->timeout(15) ->post($this->base().'/plan-checkout/verify', [ 'reference' => $reference, ]); if ($res->status() === 422) { return ['paid' => false, 'error' => $res->json('error')]; } $res->throw(); return $res->json(); } /** * Write suite messaging plan entitlement (SoT for included email/SMS allowances). * * @param array{period_starts_at?: string, period_ends_at?: string|null, source?: string, metadata?: array} $options */ public function syncSuiteEntitlement( string $publicId, string $plan, string $status, string $reference, array $options = [], ): bool { if (! $this->configured()) { return false; } $app = (string) config('billing.service', 'events'); $starts = $options['period_starts_at'] ?? now()->toIso8601String(); $ends = array_key_exists('period_ends_at', $options) ? $options['period_ends_at'] : now()->addDays(30)->toIso8601String(); try { $res = Http::withToken($this->token()) ->acceptJson() ->timeout(15) ->post($this->base().'/suite-entitlements', array_filter([ 'user' => $publicId, 'app' => $app, 'plan' => $plan, 'status' => $status, 'period_starts_at' => $starts, 'period_ends_at' => $ends, 'source' => $options['source'] ?? 'wallet_debit', 'reference' => $reference, 'metadata' => $options['metadata'] ?? null, ], static fn ($v) => $v !== null)); if ($res->failed()) { Log::warning('Events suite entitlement sync failed', [ 'user' => $publicId, 'status' => $res->status(), 'body' => $res->body(), ]); return false; } return true; } catch (\Throwable $e) { Log::warning('Events suite entitlement sync error', ['error' => $e->getMessage()]); return false; } } }