Speed up QR create/update by cutting billing round-trips.
Deploy Ladill QR Plus / deploy (push) Successful in 2m2s

Create was making three sequential public HTTPS billing calls (~400ms each)
while holding a DB transaction open for image generation. Use loopback DNS
forcing, cache balances briefly, return balance from debit, regenerate images
only when style changes, and keep remote work outside the DB transaction.
This commit is contained in:
isaacclad
2026-07-16 21:25:16 +00:00
parent d83a314bee
commit 02e6778ccf
7 changed files with 261 additions and 64 deletions
+40 -9
View File
@@ -36,6 +36,8 @@ class QrCodeManagerService
{
$wallet = $this->walletFor($user);
// Fast path: one cached balance read. Debit is still authoritative (402 if
// funds are short). Avoids a second can-afford HTTP round-trip.
if (! $wallet->canCreateQr()) {
throw new RuntimeException('Add at least GHS ' . number_format(QrWallet::pricePerQr(), 2) . ' to your QR wallet before creating codes.');
}
@@ -44,7 +46,8 @@ class QrCodeManagerService
$validated = $this->payloadValidator->validateForCreate($type, $data);
$style = QrStyleDefaults::merge($data['style'] ?? null);
return DB::transaction(function () use ($user, $wallet, $data, $type, $validated, $style) {
// Persist the code first (short DB transaction — no remote billing/image work).
$qrCode = DB::transaction(function () use ($user, $data, $type, $validated, $style) {
$documentId = null;
$content = $validated['content'];
@@ -175,7 +178,7 @@ class QrCodeManagerService
$payload['wifi_encoded'] = \App\Support\Qr\QrWifiPayload::encode($content);
}
$qrCode = QrCode::create([
return QrCode::create([
'user_id' => $user->id,
'short_code' => $shortCode,
'type' => $type,
@@ -186,12 +189,20 @@ class QrCodeManagerService
'is_active' => true,
'destination_updated_at' => now(),
]);
$this->billing->debitForQrCreation($wallet, $qrCode);
$this->imageGenerator->generateAndStore($qrCode);
return $qrCode->fresh(['document']);
});
try {
$this->billing->debitForQrCreation($wallet, $qrCode);
} catch (RuntimeException $e) {
// Roll back the unpaid code so a failed debit cannot leave a free QR.
$qrCode->delete();
throw $e;
}
// Image render is local CPU work — keep it outside the DB transaction.
$this->imageGenerator->generateAndStore($qrCode);
return $qrCode->fresh(['document']);
}
/**
@@ -331,9 +342,10 @@ class QrCodeManagerService
$content['cover_path'] = $this->storeMenuBrandImage($qrCode->user, $data['itinerary_cover'], 'itinerary-covers');
}
$previousStyle = $style;
if (isset($data['style']) && is_array($data['style'])) {
$style = QrStyleDefaults::merge(array_merge($style, $data['style']));
$regenerate = true;
}
if (($data['logo'] ?? null) instanceof UploadedFile) {
@@ -341,11 +353,16 @@ class QrCodeManagerService
$regenerate = true;
}
if (($data['remove_logo'] ?? false) && $style['logo_path']) {
if (($data['remove_logo'] ?? false) && ($style['logo_path'] ?? null)) {
$style['logo_path'] = null;
$regenerate = true;
}
// Only re-render the PNG/SVG when style actually changed (forms always post style[]).
if (! $regenerate && $this->styleChanged($previousStyle, $style)) {
$regenerate = true;
}
$qrCode->destination_url = $destinationUrl;
// Preserve frozen keys (e.g. wifi_encoded) so the printed WiFi code stays put.
$payload = (array) ($qrCode->payload ?? []);
@@ -361,6 +378,20 @@ class QrCodeManagerService
return $qrCode->fresh(['document']);
}
/**
* @param array<string, mixed> $before
* @param array<string, mixed> $after
*/
private function styleChanged(array $before, array $after): bool
{
$before = QrStyleDefaults::merge($before);
$after = QrStyleDefaults::merge($after);
ksort($before);
ksort($after);
return $before !== $after;
}
/**
* Inject uploaded item images into the sections array.
* Form field: item_images[sectionIndex][itemIndex] (UploadedFile)