-
Notifications
You must be signed in to change notification settings - Fork 0
Release v0.1.15 — Phase 15: Conversion Cost Estimator #205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c6e496c
978b5fb
7912d00
b41f371
8d2f33d
0f6bfea
db2de27
b623a95
a668eee
70ce0bd
d7611c0
e737081
db7ed52
9ea385e
eba9622
20d2b62
92e6f04
3c2c0f8
c990843
f9d9add
0d2622a
2f51528
8a84417
f877c5b
14f6e2b
facc74e
59ec259
2cc88b9
b787f32
1717d07
7818b80
cf6bfaa
4e2d5f3
09c04ca
c6c3224
e26372f
f5befa1
9f0ffc3
fda6150
8d06c8a
c08985b
b969ca1
9d5f474
89413c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Actions\Conversions; | ||
|
|
||
| use App\Contracts\Billing\ConversionCostEstimator; | ||
| use App\Data\Credits\CreditCost; | ||
| use App\Models\FileRecord; | ||
| use App\Support\Converters\Contracts\Converter; | ||
|
|
||
| final class EstimateConversionCostAction | ||
| { | ||
| public function __construct( | ||
| private readonly ConversionCostEstimator $estimator, | ||
| ) {} | ||
|
|
||
| /** | ||
| * @param array<string, mixed> $options | ||
| */ | ||
| public function handle(FileRecord $file, Converter $converter, array $options = []): CreditCost | ||
| { | ||
| return $this->estimator->estimate($file, $converter, $options); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Contracts\Billing; | ||
|
|
||
| use App\Data\Credits\CreditCost; | ||
| use App\Models\FileRecord; | ||
| use App\Support\Converters\Contracts\Converter; | ||
|
|
||
| interface ConversionCostEstimator | ||
| { | ||
| /** | ||
| * @param array<string, mixed> $options | ||
| */ | ||
| public function estimate(FileRecord $file, Converter $converter, array $options = []): CreditCost; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Data\Credits; | ||
|
|
||
| final readonly class CreditCost | ||
| { | ||
| public function __construct( | ||
| public int $amount, | ||
| public array $breakdown, | ||
| ) { | ||
| if ($this->amount < 0) { | ||
| throw new \InvalidArgumentException('Credit cost amount cannot be negative.'); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Data\Credits; | ||
|
|
||
| final readonly class CreditCostBreakdown | ||
| { | ||
| public function __construct( | ||
| public int $base, | ||
| public int $size, | ||
| public int $features, | ||
| public int $total, | ||
| public array $details, | ||
| ) { | ||
| if ($this->base < 0) { | ||
| throw new \InvalidArgumentException('Breakdown base cannot be negative.'); | ||
| } | ||
| if ($this->size < 0) { | ||
| throw new \InvalidArgumentException('Breakdown size cannot be negative.'); | ||
| } | ||
| if ($this->features < 0) { | ||
| throw new \InvalidArgumentException('Breakdown features cannot be negative.'); | ||
| } | ||
| if ($this->total < 0) { | ||
| throw new \InvalidArgumentException('Breakdown total cannot be negative.'); | ||
| } | ||
| if ($this->total !== ($this->base + $this->size + $this->features)) { | ||
| throw new \InvalidArgumentException( | ||
| "Breakdown total ({$this->total}) must equal base + size + features ({$this->base} + {$this->size} + {$this->features})." | ||
| ); | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| public function toArray(): array | ||
| { | ||
| return [ | ||
| 'base' => $this->base, | ||
| 'size' => $this->size, | ||
| 'features' => $this->features, | ||
| 'total' => $this->total, | ||
| 'details' => $this->details, | ||
| ]; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Enums; | ||
|
|
||
| enum ConversionCreditChargeStatus: string | ||
| { | ||
| case Estimated = 'estimated'; | ||
| case Captured = 'captured'; | ||
| case Refunded = 'refunded'; | ||
| case Failed = 'failed'; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Exceptions\Billing; | ||
|
|
||
| use DomainException; | ||
|
|
||
| final class UnsupportedConversionCostException extends DomainException | ||
| { | ||
| public static function forPair(string $source, string $target): self | ||
| { | ||
| return new self("Cannot estimate cost for unsupported conversion: {$source} → {$target}."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,8 +3,11 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace App\Livewire\Dashboard; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Actions\Conversions\CreateConversionJobAction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Actions\Conversions\EstimateConversionCostAction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Actions\Files\StoreUploadedFileAction; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Enums\ConversionStatus; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Exceptions\Billing\InsufficientCreditsException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Exceptions\Billing\UnsupportedConversionCostException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Exceptions\Files\FileStorageException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Exceptions\Files\UnsupportedFileFormatException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Exceptions\Storage\StorageLimitExceededException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -60,6 +63,8 @@ class DashboardConverter extends Component | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ?string $convertError = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ?int $estimatedCreditCost = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public function updatedUpload(): void | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->storeUpload(app(StoreUploadedFileAction::class), app(FeatureAccessService::class)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -190,6 +195,18 @@ public function selectTargetFormat(string $targetFormat): void | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->initializeOptionsFromSchema(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $cost = app(EstimateConversionCostAction::class)->handle( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->currentFile, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $converter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->options, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->estimatedCreditCost = $cost->amount; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (UnsupportedConversionCostException|\InvalidArgumentException) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Known pricing failures — unsupported pair or misconfigured rule. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->estimatedCreditCost = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+198
to
+208
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't swallow every estimation failure and continue to settings. Line 204 catches all Proposed fix+use App\Exceptions\Billing\UnsupportedConversionCostException;
+
try {
$cost = app(EstimateConversionCostAction::class)->handle(
$this->currentFile,
$converter,
$this->options,
);
$this->estimatedCreditCost = $cost->amount;
- } catch (Throwable) {
+ } catch (UnsupportedConversionCostException) {
$this->estimatedCreditCost = null;
+ $this->targetFormatError = 'We could not estimate credits for this conversion yet.';
+ $this->step = 'format';
+
+ return;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->step = 'settings'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -244,6 +261,12 @@ public function convert(): void | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->convertError = 'This conversion is not supported. Please choose a different format.'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->step = 'format'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (InsufficientCreditsException $e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->convertError = 'Not enough credits. This conversion requires ' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .($this->estimatedCreditCost ?? '?') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .' credit(s). Please top up your balance.'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Throwable) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->convertError = 'Something went wrong. Please try again.'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -338,6 +361,7 @@ private function resetTargetSelection(): void | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->optionsSchema = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->options = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->optionsByTarget = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->estimatedCreditCost = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public function getCurrentJobProperty(): ?ConversionJob | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Credit enforcement uses a non-atomic balance pre-check, so concurrent requests can over-queue jobs and fail later during capture.
Prompt for AI agents