coupon code system eingeführt. coupons werden vom super admin gemanaged. coupons werden mit paddle synchronisiert und dort validiert. plus: einige mobil-optimierungen im tenant admin pwa.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Services\Checkout;
|
||||
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\Coupon;
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
@@ -64,6 +65,7 @@ class CheckoutSessionService
|
||||
$session->package_snapshot = $this->packageSnapshot($package);
|
||||
$session->amount_subtotal = Arr::get($session->package_snapshot, 'price', 0);
|
||||
$session->amount_total = Arr::get($session->package_snapshot, 'price', 0);
|
||||
$session->amount_discount = 0;
|
||||
$session->provider = CheckoutSession::PROVIDER_NONE;
|
||||
$session->status = CheckoutSession::STATUS_DRAFT;
|
||||
$session->stripe_payment_intent_id = null;
|
||||
@@ -73,6 +75,10 @@ class CheckoutSessionService
|
||||
$session->paddle_transaction_id = null;
|
||||
$session->provider_metadata = [];
|
||||
$session->failure_reason = null;
|
||||
$session->coupon()->dissociate();
|
||||
$session->coupon_code = null;
|
||||
$session->coupon_snapshot = [];
|
||||
$session->discount_breakdown = [];
|
||||
$session->expires_at = now()->addMinutes($this->sessionTtlMinutes);
|
||||
$this->appendStatus($session, CheckoutSession::STATUS_DRAFT, 'package_switched');
|
||||
$session->save();
|
||||
@@ -81,6 +87,31 @@ class CheckoutSessionService
|
||||
});
|
||||
}
|
||||
|
||||
public function applyCoupon(CheckoutSession $session, Coupon $coupon, array $pricing): CheckoutSession
|
||||
{
|
||||
$snapshot = [
|
||||
'coupon' => [
|
||||
'id' => $coupon->id,
|
||||
'code' => $coupon->code,
|
||||
'type' => $coupon->type?->value,
|
||||
],
|
||||
'pricing' => $pricing,
|
||||
];
|
||||
|
||||
$session->coupon()->associate($coupon);
|
||||
$session->coupon_code = $coupon->code;
|
||||
$session->coupon_snapshot = $snapshot;
|
||||
$session->amount_subtotal = $pricing['subtotal'] ?? $session->amount_subtotal;
|
||||
$session->amount_discount = $pricing['discount'] ?? 0;
|
||||
$session->amount_total = $pricing['total'] ?? $session->amount_total;
|
||||
$session->discount_breakdown = is_array($pricing['breakdown'] ?? null)
|
||||
? $pricing['breakdown']
|
||||
: [];
|
||||
$session->save();
|
||||
|
||||
return $session->refresh();
|
||||
}
|
||||
|
||||
public function selectProvider(CheckoutSession $session, string $provider): CheckoutSession
|
||||
{
|
||||
$provider = strtolower($provider);
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\CheckoutSession;
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Services\Coupons\CouponRedemptionService;
|
||||
use App\Services\Paddle\PaddleSubscriptionService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
@@ -19,6 +20,7 @@ class CheckoutWebhookService
|
||||
private readonly CheckoutSessionService $sessions,
|
||||
private readonly CheckoutAssignmentService $assignment,
|
||||
private readonly PaddleSubscriptionService $paddleSubscriptions,
|
||||
private readonly CouponRedemptionService $couponRedemptions,
|
||||
) {}
|
||||
|
||||
public function handleStripeEvent(array $event): bool
|
||||
@@ -216,6 +218,7 @@ class CheckoutWebhookService
|
||||
]);
|
||||
|
||||
$this->sessions->markCompleted($session, now());
|
||||
$this->couponRedemptions->recordSuccess($session, $data);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -224,6 +227,7 @@ class CheckoutWebhookService
|
||||
case 'transaction.cancelled':
|
||||
$reason = $status ?: ($eventType === 'transaction.failed' ? 'paddle_failed' : 'paddle_cancelled');
|
||||
$this->sessions->markFailed($session, $reason);
|
||||
$this->couponRedemptions->recordFailure($session, $reason);
|
||||
|
||||
return true;
|
||||
|
||||
|
||||
72
app/Services/Coupons/CouponRedemptionService.php
Normal file
72
app/Services/Coupons/CouponRedemptionService.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Coupons;
|
||||
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\CouponRedemption;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class CouponRedemptionService
|
||||
{
|
||||
public function recordSuccess(CheckoutSession $session, array $payload = []): void
|
||||
{
|
||||
if (! $session->coupon_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transactionId = Arr::get($payload, 'id') ?? $session->paddle_transaction_id;
|
||||
|
||||
$values = [
|
||||
'tenant_id' => $session->tenant_id,
|
||||
'user_id' => $session->user_id,
|
||||
'package_id' => $session->package_id,
|
||||
'paddle_transaction_id' => $transactionId,
|
||||
'status' => CouponRedemption::STATUS_SUCCESS,
|
||||
'failure_reason' => null,
|
||||
'amount_discounted' => $session->amount_discount,
|
||||
'currency' => $session->currency ?? 'EUR',
|
||||
'metadata' => array_filter([
|
||||
'session_snapshot' => $session->coupon_snapshot,
|
||||
'payload' => $payload,
|
||||
]),
|
||||
'redeemed_at' => now(),
|
||||
];
|
||||
|
||||
CouponRedemption::query()->updateOrCreate(
|
||||
[
|
||||
'coupon_id' => $session->coupon_id,
|
||||
'checkout_session_id' => $session->id,
|
||||
],
|
||||
$values,
|
||||
);
|
||||
|
||||
$session->coupon?->increment('redemptions_count');
|
||||
}
|
||||
|
||||
public function recordFailure(CheckoutSession $session, string $reason): void
|
||||
{
|
||||
if (! $session->coupon_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
CouponRedemption::query()->updateOrCreate(
|
||||
[
|
||||
'coupon_id' => $session->coupon_id,
|
||||
'checkout_session_id' => $session->id,
|
||||
],
|
||||
[
|
||||
'tenant_id' => $session->tenant_id,
|
||||
'user_id' => $session->user_id,
|
||||
'package_id' => $session->package_id,
|
||||
'paddle_transaction_id' => $session->paddle_transaction_id,
|
||||
'status' => CouponRedemption::STATUS_FAILED,
|
||||
'failure_reason' => $reason,
|
||||
'amount_discounted' => $session->amount_discount,
|
||||
'currency' => $session->currency ?? 'EUR',
|
||||
'metadata' => array_filter([
|
||||
'session_snapshot' => $session->coupon_snapshot,
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
287
app/Services/Coupons/CouponService.php
Normal file
287
app/Services/Coupons/CouponService.php
Normal file
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Coupons;
|
||||
|
||||
use App\Enums\CouponStatus;
|
||||
use App\Enums\CouponType;
|
||||
use App\Models\Coupon;
|
||||
use App\Models\CouponRedemption;
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Paddle\Exceptions\PaddleException;
|
||||
use App\Services\Paddle\PaddleDiscountService;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class CouponService
|
||||
{
|
||||
public function __construct(private readonly PaddleDiscountService $paddleDiscounts) {}
|
||||
|
||||
/**
|
||||
* @return array{coupon: Coupon, pricing: array<string, mixed>, source: string}
|
||||
*/
|
||||
public function preview(string $code, Package $package, ?Tenant $tenant = null): array
|
||||
{
|
||||
$coupon = $this->findCouponForCode($code);
|
||||
|
||||
$this->ensureCouponCanBeApplied($coupon, $package, $tenant);
|
||||
|
||||
$pricing = $this->buildPricingBreakdown($coupon, $package, $tenant);
|
||||
|
||||
return [
|
||||
'coupon' => $coupon,
|
||||
'pricing' => $pricing['pricing'],
|
||||
'source' => $pricing['source'],
|
||||
];
|
||||
}
|
||||
|
||||
public function ensureCouponCanBeApplied(Coupon $coupon, Package $package, ?Tenant $tenant = null): void
|
||||
{
|
||||
if (! $coupon->paddle_discount_id) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('marketing.coupon.errors.not_synced'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $coupon->enabled_for_checkout) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('marketing.coupon.errors.disabled'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $coupon->isCurrentlyActive()) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('marketing.coupon.errors.inactive'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $coupon->appliesToPackage($package)) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('marketing.coupon.errors.not_applicable'),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($tenant) {
|
||||
$usage = $this->usageForTenant($coupon, $tenant);
|
||||
$remaining = $coupon->remainingUsages($usage);
|
||||
|
||||
if ($remaining !== null && $remaining <= 0) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('marketing.coupon.errors.limit_reached'),
|
||||
]);
|
||||
}
|
||||
} elseif ($coupon->per_customer_limit !== null) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('marketing.coupon.errors.login_required'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function findCouponForCode(string $code): Coupon
|
||||
{
|
||||
$normalized = Str::upper(trim($code));
|
||||
|
||||
if ($normalized === '') {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('marketing.coupon.errors.required'),
|
||||
]);
|
||||
}
|
||||
|
||||
$coupon = Coupon::query()
|
||||
->where('code', $normalized)
|
||||
->first();
|
||||
|
||||
if (! $coupon) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('marketing.coupon.errors.not_found'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (in_array($coupon->status, [CouponStatus::PAUSED, CouponStatus::ARCHIVED, CouponStatus::DRAFT], true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('marketing.coupon.errors.inactive'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $coupon;
|
||||
}
|
||||
|
||||
protected function usageForTenant(Coupon $coupon, Tenant $tenant): int
|
||||
{
|
||||
return $coupon->redemptions()
|
||||
->where('tenant_id', $tenant->id)
|
||||
->whereNot('status', CouponRedemption::STATUS_FAILED)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{pricing: array<string, mixed>, source: string}
|
||||
*/
|
||||
protected function buildPricingBreakdown(Coupon $coupon, Package $package, ?Tenant $tenant = null): array
|
||||
{
|
||||
$currency = Str::upper($package->currency ?? 'EUR');
|
||||
$subtotal = (float) $package->price;
|
||||
|
||||
if ($package->paddle_price_id) {
|
||||
try {
|
||||
$preview = $this->paddleDiscounts->previewDiscount(
|
||||
$coupon,
|
||||
[
|
||||
[
|
||||
'price_id' => $package->paddle_price_id,
|
||||
'quantity' => 1,
|
||||
],
|
||||
],
|
||||
array_filter([
|
||||
'currency' => $currency,
|
||||
'customer_id' => $tenant?->paddle_customer_id,
|
||||
])
|
||||
);
|
||||
|
||||
$mapped = $this->mapPaddlePreview($preview, $currency, $subtotal);
|
||||
|
||||
return [
|
||||
'pricing' => $mapped,
|
||||
'source' => 'paddle',
|
||||
];
|
||||
} catch (PaddleException $exception) {
|
||||
Log::warning('Paddle preview failed, falling back to manual pricing', [
|
||||
'coupon_id' => $coupon->id,
|
||||
'package_id' => $package->id,
|
||||
'message' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'pricing' => $this->manualPricing($coupon, $currency, $subtotal),
|
||||
'source' => 'manual',
|
||||
];
|
||||
}
|
||||
|
||||
protected function mapPaddlePreview(array $preview, string $currency, float $fallbackSubtotal): array
|
||||
{
|
||||
$totals = $this->extractTotals($preview);
|
||||
|
||||
$subtotal = $totals['subtotal'] ?? $fallbackSubtotal;
|
||||
$discount = $totals['discount'] ?? 0.0;
|
||||
$tax = $totals['tax'] ?? 0.0;
|
||||
$total = $totals['total'] ?? max($subtotal - $discount + $tax, 0);
|
||||
|
||||
return $this->formatPricing($currency, $subtotal, $discount, $tax, $total, [
|
||||
'raw' => $preview,
|
||||
'breakdown' => $totals['breakdown'] ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function manualPricing(Coupon $coupon, string $currency, float $subtotal): array
|
||||
{
|
||||
$discount = match ($coupon->type) {
|
||||
CouponType::PERCENTAGE => round($subtotal * ((float) $coupon->amount) / 100, 2),
|
||||
default => (float) $coupon->amount,
|
||||
};
|
||||
|
||||
if ($coupon->type !== CouponType::PERCENTAGE && $coupon->currency && Str::upper($coupon->currency) !== $currency) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('marketing.coupon.errors.currency_mismatch'),
|
||||
]);
|
||||
}
|
||||
|
||||
$discount = min($discount, $subtotal);
|
||||
$total = max($subtotal - $discount, 0);
|
||||
|
||||
return $this->formatPricing($currency, $subtotal, $discount, 0, $total, [
|
||||
'breakdown' => [
|
||||
['type' => 'coupon', 'amount' => $discount],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function extractTotals(array $preview): array
|
||||
{
|
||||
$totals = Arr::get($preview, 'totals', Arr::get($preview, 'details.totals', []));
|
||||
|
||||
$subtotal = $this->convertMinorAmount($totals['subtotal'] ?? ($totals['subtotal']['amount'] ?? null));
|
||||
$discount = $this->convertMinorAmount($totals['discount'] ?? ($totals['discount']['amount'] ?? null));
|
||||
$tax = $this->convertMinorAmount($totals['tax'] ?? ($totals['tax']['amount'] ?? null));
|
||||
$total = $this->convertMinorAmount($totals['total'] ?? ($totals['total']['amount'] ?? null));
|
||||
|
||||
return array_filter([
|
||||
'currency' => $totals['currency_code'] ?? Arr::get($preview, 'currency_code'),
|
||||
'subtotal' => $subtotal,
|
||||
'discount' => $discount,
|
||||
'tax' => $tax,
|
||||
'total' => $total,
|
||||
'breakdown' => Arr::get($preview, 'discounts', []),
|
||||
], static fn ($value) => $value !== null && $value !== '');
|
||||
}
|
||||
|
||||
protected function convertMinorAmount(mixed $value): ?float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($value) && isset($value['amount'])) {
|
||||
$value = $value['amount'];
|
||||
}
|
||||
|
||||
if (! is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round(((float) $value) / 100, 2);
|
||||
}
|
||||
|
||||
protected function formatPricing(string $currency, float $subtotal, float $discount, float $tax, float $total, array $extra = []): array
|
||||
{
|
||||
$locale = $this->mapLocale(app()->getLocale());
|
||||
$formatter = class_exists(\NumberFormatter::class)
|
||||
? new \NumberFormatter($locale, \NumberFormatter::CURRENCY)
|
||||
: null;
|
||||
|
||||
$format = function (float $amount, bool $allowNegative = false) use ($currency, $formatter): string {
|
||||
$value = $allowNegative ? $amount : max($amount, 0);
|
||||
|
||||
if ($formatter) {
|
||||
$formatted = $formatter->formatCurrency($value, $currency);
|
||||
if ($formatted !== false) {
|
||||
return $formatted;
|
||||
}
|
||||
}
|
||||
|
||||
$symbol = match ($currency) {
|
||||
'EUR' => '€',
|
||||
'USD' => '$',
|
||||
default => $currency.' ',
|
||||
};
|
||||
|
||||
return $symbol.number_format($value, 2, ',', '.');
|
||||
};
|
||||
|
||||
return array_merge([
|
||||
'currency' => $currency,
|
||||
'subtotal' => round($subtotal, 2),
|
||||
'discount' => round($discount, 2),
|
||||
'tax' => round($tax, 2),
|
||||
'total' => round($total, 2),
|
||||
'formatted' => [
|
||||
'subtotal' => $format($subtotal),
|
||||
'discount' => $format(-1 * abs($discount), true),
|
||||
'tax' => $format($tax),
|
||||
'total' => $format($total),
|
||||
],
|
||||
], $extra);
|
||||
}
|
||||
|
||||
protected function mapLocale(?string $locale): string
|
||||
{
|
||||
return match ($locale) {
|
||||
'de', 'de_DE' => 'de_DE',
|
||||
'en', 'en_GB', 'en_US' => 'en_US',
|
||||
default => 'en_US',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class PaddleCheckoutService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array{success_url?: string|null, return_url?: string|null} $options
|
||||
* @param array{success_url?: string|null, return_url?: string|null, discount_id?: string|null, metadata?: array} $options
|
||||
*/
|
||||
public function createCheckout(Tenant $tenant, Package $package, array $options = []): array
|
||||
{
|
||||
@@ -46,6 +46,10 @@ class PaddleCheckoutService
|
||||
'cancel_url' => $returnUrl,
|
||||
];
|
||||
|
||||
if (! empty($options['discount_id'])) {
|
||||
$payload['discount_id'] = $options['discount_id'];
|
||||
}
|
||||
|
||||
if ($tenant->contact_email) {
|
||||
$payload['customer_email'] = $tenant->contact_email;
|
||||
}
|
||||
|
||||
149
app/Services/Paddle/PaddleDiscountService.php
Normal file
149
app/Services/Paddle/PaddleDiscountService.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Paddle;
|
||||
|
||||
use App\Enums\CouponType;
|
||||
use App\Models\Coupon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PaddleDiscountService
|
||||
{
|
||||
public function __construct(private readonly PaddleClient $client) {}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createDiscount(Coupon $coupon): array
|
||||
{
|
||||
$payload = $this->buildDiscountPayload($coupon);
|
||||
|
||||
$response = $this->client->post('/discounts', $payload);
|
||||
|
||||
return Arr::get($response, 'data', $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function updateDiscount(Coupon $coupon): array
|
||||
{
|
||||
if (! $coupon->paddle_discount_id) {
|
||||
return $this->createDiscount($coupon);
|
||||
}
|
||||
|
||||
$payload = $this->buildDiscountPayload($coupon);
|
||||
|
||||
$response = $this->client->patch('/discounts/'.$coupon->paddle_discount_id, $payload);
|
||||
|
||||
return Arr::get($response, 'data', $response);
|
||||
}
|
||||
|
||||
public function archiveDiscount(Coupon $coupon): void
|
||||
{
|
||||
if (! $coupon->paddle_discount_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->client->delete('/discounts/'.$coupon->paddle_discount_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{price_id: string, quantity?: int}> $items
|
||||
* @param array{currency?: string, address?: array{country_code: string, postal_code?: string}, customer_id?: string, address_id?: string} $context
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function previewDiscount(Coupon $coupon, array $items, array $context = []): array
|
||||
{
|
||||
$payload = [
|
||||
'items' => $items,
|
||||
'discount_id' => $coupon->paddle_discount_id,
|
||||
];
|
||||
|
||||
if (isset($context['currency'])) {
|
||||
$payload['currency_code'] = Str::upper($context['currency']);
|
||||
}
|
||||
|
||||
if (isset($context['address'])) {
|
||||
$payload['address'] = $context['address'];
|
||||
}
|
||||
|
||||
if (isset($context['customer_id'])) {
|
||||
$payload['customer_id'] = $context['customer_id'];
|
||||
}
|
||||
|
||||
if (isset($context['address_id'])) {
|
||||
$payload['address_id'] = $context['address_id'];
|
||||
}
|
||||
|
||||
$response = $this->client->post('/transactions/preview', $payload);
|
||||
|
||||
return Arr::get($response, 'data', $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function buildDiscountPayload(Coupon $coupon): array
|
||||
{
|
||||
$payload = [
|
||||
'name' => $coupon->name,
|
||||
'code' => $coupon->code,
|
||||
'type' => $this->mapType($coupon->type),
|
||||
'amount' => $this->formatAmount($coupon),
|
||||
'currency_code' => $coupon->currency ?? config('app.currency', 'EUR'),
|
||||
'enabled_for_checkout' => $coupon->enabled_for_checkout,
|
||||
'description' => $coupon->description,
|
||||
'mode' => $coupon->paddle_mode ?? 'standard',
|
||||
'usage_limit' => $coupon->usage_limit,
|
||||
'maximum_recurring_intervals' => null,
|
||||
'recur' => false,
|
||||
'restrict_to' => $this->resolveRestrictions($coupon),
|
||||
'starts_at' => optional($coupon->starts_at)?->toIso8601String(),
|
||||
'expires_at' => optional($coupon->ends_at)?->toIso8601String(),
|
||||
];
|
||||
|
||||
if ($payload['type'] === 'percentage') {
|
||||
unset($payload['currency_code']);
|
||||
}
|
||||
|
||||
return Collection::make($payload)
|
||||
->reject(static fn ($value) => $value === null || $value === '')
|
||||
->all();
|
||||
}
|
||||
|
||||
protected function formatAmount(Coupon $coupon): string
|
||||
{
|
||||
if ($coupon->type === CouponType::PERCENTAGE) {
|
||||
return (string) $coupon->amount;
|
||||
}
|
||||
|
||||
return (string) ((int) round($coupon->amount * 100));
|
||||
}
|
||||
|
||||
protected function mapType(CouponType $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
CouponType::PERCENTAGE => 'percentage',
|
||||
CouponType::FLAT => 'flat',
|
||||
CouponType::FLAT_PER_SEAT => 'flat_per_seat',
|
||||
};
|
||||
}
|
||||
|
||||
protected function resolveRestrictions(Coupon $coupon): ?array
|
||||
{
|
||||
$packages = ($coupon->relationLoaded('packages')
|
||||
? $coupon->packages
|
||||
: $coupon->packages()->get())
|
||||
->whereNotNull('paddle_price_id');
|
||||
|
||||
if ($packages->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$prices = $packages->pluck('paddle_price_id')->filter()->values();
|
||||
|
||||
return $prices->isEmpty() ? null : $prices->all();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user