205 lines
5.8 KiB
PHP
205 lines
5.8 KiB
PHP
<?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
|
|
{
|
|
$existing = $this->findExistingDiscount($coupon->code);
|
|
if ($existing !== null) {
|
|
return $existing;
|
|
}
|
|
|
|
$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>|null
|
|
*/
|
|
protected function findExistingDiscount(?string $code): ?array
|
|
{
|
|
$normalized = Str::upper(trim((string) $code));
|
|
if ($normalized === '') {
|
|
return null;
|
|
}
|
|
|
|
$response = $this->client->get('/discounts', [
|
|
'code' => $normalized,
|
|
'per_page' => 1,
|
|
]);
|
|
|
|
$items = Arr::get($response, 'data', []);
|
|
if (! is_array($items) || $items === []) {
|
|
return null;
|
|
}
|
|
|
|
$match = Collection::make($items)->first(static function ($item) use ($normalized) {
|
|
$codeValue = Str::upper((string) Arr::get($item, 'code', ''));
|
|
|
|
return $codeValue === $normalized ? $item : null;
|
|
});
|
|
|
|
return is_array($match) ? $match : null;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function buildDiscountPayload(Coupon $coupon): array
|
|
{
|
|
$payload = [
|
|
'code' => $coupon->code,
|
|
'type' => $this->mapType($coupon->type),
|
|
'amount' => $this->formatAmount($coupon),
|
|
'currency_code' => Str::upper((string) ($coupon->currency ?? config('app.currency', 'EUR'))),
|
|
'enabled_for_checkout' => $coupon->enabled_for_checkout,
|
|
'description' => $this->resolveDescription($coupon),
|
|
'mode' => $coupon->paddle_mode ?? 'standard',
|
|
'usage_limit' => $coupon->usage_limit,
|
|
'maximum_recurring_intervals' => null,
|
|
'recur' => false,
|
|
'restrict_to' => $this->resolveRestrictions($coupon),
|
|
'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();
|
|
}
|
|
|
|
protected function resolveDescription(Coupon $coupon): string
|
|
{
|
|
$description = trim((string) ($coupon->description ?? ''));
|
|
|
|
if ($description !== '') {
|
|
return $description;
|
|
}
|
|
|
|
$name = trim((string) ($coupon->name ?? ''));
|
|
|
|
if ($name !== '') {
|
|
return $name;
|
|
}
|
|
|
|
$code = trim((string) ($coupon->code ?? ''));
|
|
|
|
if ($code !== '') {
|
|
return sprintf('Coupon %s', $code);
|
|
}
|
|
|
|
return sprintf('Coupon %d', $coupon->id);
|
|
}
|
|
}
|