Initial commit: Runware-powered content generator foundation

Laravel app with a cost-tracked Runware.ai integration (image, video,
text generation), a hard monthly EUR budget cap with live USD/EUR FX
conversion, and a locally-maintained model/pricing table since
Runware's model-search API doesn't expose pricing.
This commit is contained in:
2026-09-23 15:59:05 +03:00
commit 79320950e7
78 changed files with 14452 additions and 0 deletions
@@ -0,0 +1,53 @@
<?php
namespace App\Services\Runware;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Keeps a live-ish USD->EUR rate cached for BudgetGuard to convert Runware's
* USD costs against the EUR monthly cap. Refreshed daily by the scheduler
* (see routes/console.php); falls back to a static configured rate if no
* live fetch has ever succeeded, so the cap never stops enforcing.
*/
class CurrencyRateService
{
protected const CACHE_KEY = 'runware:usd_eur_rate';
// Refresh runs daily; keep the cached value alive well past that so a
// single failed fetch doesn't drop back to the fallback rate.
protected const CACHE_TTL_SECONDS = 60 * 60 * 30;
public function usdToEur(): float
{
return Cache::get(self::CACHE_KEY) ?? (float) config('runware.usd_eur_fallback_rate');
}
public function refresh(): ?float
{
try {
$response = Http::timeout(10)->get('https://api.frankfurter.app/latest', [
'from' => 'USD',
'to' => 'EUR',
]);
$rate = $response->json('rates.EUR');
if (! $response->successful() || ! is_numeric($rate)) {
throw new \RuntimeException('Unexpected FX response: '.$response->body());
}
Cache::put(self::CACHE_KEY, (float) $rate, self::CACHE_TTL_SECONDS);
return (float) $rate;
} catch (\Throwable $e) {
Log::warning('Failed to refresh USD/EUR rate, keeping previous cached/fallback value.', [
'exception' => $e->getMessage(),
]);
return null;
}
}
}