Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s
Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ResellerClub;
|
||||
|
||||
use App\Models\Domain;
|
||||
use App\Models\PromoBanner;
|
||||
use App\Models\RcServiceOrder;
|
||||
use App\Models\User;
|
||||
use App\Services\Domain\DomainRegistrarService;
|
||||
use App\Support\ResellerClubLegacy;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use RuntimeException;
|
||||
|
||||
class RcOrderCartService
|
||||
{
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function cartStatuses(): array
|
||||
{
|
||||
return [
|
||||
RcServiceOrder::STATUS_CART,
|
||||
RcServiceOrder::STATUS_PENDING_PAYMENT,
|
||||
];
|
||||
}
|
||||
|
||||
public function cartItemsForUser(User $user): EloquentCollection
|
||||
{
|
||||
return RcServiceOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereIn('status', $this->cartStatuses())
|
||||
->latest()
|
||||
->get();
|
||||
}
|
||||
|
||||
public function cartCountForUser(User $user): int
|
||||
{
|
||||
return RcServiceOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereIn('status', $this->cartStatuses())
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{items: EloquentCollection<int, RcServiceOrder>, count: int, reference: string|null}|null
|
||||
*/
|
||||
public function pendingCheckoutForUser(User $user): ?array
|
||||
{
|
||||
$items = RcServiceOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('status', RcServiceOrder::STATUS_PENDING_PAYMENT)
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
if ($items->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'count' => $items->count(),
|
||||
'reference' => $items->pluck('payment_reference')->filter()->first(),
|
||||
];
|
||||
}
|
||||
|
||||
public function recentOrdersForUser(User $user, int $limit = 10): EloquentCollection
|
||||
{
|
||||
return RcServiceOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereNotIn('status', $this->cartStatuses())
|
||||
->latest()
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function addProductOrder(User $user, string $category, array $input, ResellerClubProductService $products): RcServiceOrder
|
||||
{
|
||||
ResellerClubLegacy::assertNewSaleAllowed();
|
||||
|
||||
$quote = $products->quoteForSelection(
|
||||
$category,
|
||||
(string) ($input['plan_id'] ?? ''),
|
||||
(int) ($input['months'] ?? 0)
|
||||
);
|
||||
|
||||
if (! ($quote['success'] ?? false)) {
|
||||
throw new RuntimeException($quote['message'] ?? 'This product cannot be added to the cart right now.');
|
||||
}
|
||||
|
||||
$currency = strtoupper((string) ($quote['currency'] ?? 'GHS'));
|
||||
$this->assertCurrencyCompatibility($user, $currency);
|
||||
|
||||
$domainName = strtolower(trim((string) ($input['domain_name'] ?? $category)));
|
||||
|
||||
return $this->upsertCartItem(
|
||||
$user,
|
||||
[
|
||||
'category' => $category,
|
||||
'order_type' => RcServiceOrder::TYPE_SERVICE,
|
||||
'domain_name' => $domainName,
|
||||
'plan_id' => (string) ($input['plan_id'] ?? ''),
|
||||
'plan_name' => (string) ($quote['plan_name'] ?? $products->packageLabel($category, (string) ($input['plan_id'] ?? ''))),
|
||||
'amount_minor' => (int) ($quote['amount_minor'] ?? 0),
|
||||
'currency' => $currency,
|
||||
'term_months' => (int) ($input['months'] ?? 1),
|
||||
'term_years' => null,
|
||||
'meta' => [
|
||||
'order_input' => $input,
|
||||
'quote' => $quote,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function addDomainRegistration(User $user, string $domain, DomainRegistrarService $registrar): RcServiceOrder
|
||||
{
|
||||
ResellerClubLegacy::assertNewSaleAllowed();
|
||||
|
||||
$domainName = strtolower(trim($domain));
|
||||
|
||||
if (Domain::query()->where('host', $domainName)->exists()) {
|
||||
throw new RuntimeException('This domain is already registered in our system.');
|
||||
}
|
||||
|
||||
$quote = $registrar->quoteDomainRegistration($domainName);
|
||||
if (! ($quote['success'] ?? false)) {
|
||||
throw new RuntimeException($quote['message'] ?? 'Domain pricing is unavailable right now.');
|
||||
}
|
||||
|
||||
$currency = strtoupper((string) ($quote['currency'] ?? 'GHS'));
|
||||
$this->assertCurrencyCompatibility($user, $currency);
|
||||
|
||||
return $this->upsertCartItem(
|
||||
$user,
|
||||
[
|
||||
'category' => 'domain-registration',
|
||||
'order_type' => RcServiceOrder::TYPE_DOMAIN_REGISTRATION,
|
||||
'domain_name' => $domainName,
|
||||
'plan_id' => null,
|
||||
'plan_name' => '1 year registration',
|
||||
'amount_minor' => (int) ($quote['amount_minor'] ?? 0),
|
||||
'currency' => $currency,
|
||||
'term_months' => null,
|
||||
'term_years' => 1,
|
||||
'meta' => [
|
||||
'quote' => $quote,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function addDomainTransfer(User $user, string $domain, ?string $authCode, DomainRegistrarService $registrar): RcServiceOrder
|
||||
{
|
||||
ResellerClubLegacy::assertNewSaleAllowed();
|
||||
|
||||
$domainName = strtolower(trim($domain));
|
||||
$quote = $registrar->quoteDomainTransfer($domainName);
|
||||
|
||||
if (! ($quote['success'] ?? false)) {
|
||||
throw new RuntimeException($quote['message'] ?? 'This domain cannot be transferred right now.');
|
||||
}
|
||||
|
||||
$currency = strtoupper((string) ($quote['currency'] ?? 'GHS'));
|
||||
$this->assertCurrencyCompatibility($user, $currency);
|
||||
|
||||
$meta = [
|
||||
'quote' => $quote,
|
||||
];
|
||||
|
||||
$authCode = trim((string) $authCode);
|
||||
if ($authCode !== '') {
|
||||
$meta['transfer_auth_code_encrypted'] = Crypt::encryptString($authCode);
|
||||
}
|
||||
|
||||
return $this->upsertCartItem(
|
||||
$user,
|
||||
[
|
||||
'category' => 'domain-transfer',
|
||||
'order_type' => RcServiceOrder::TYPE_DOMAIN_TRANSFER,
|
||||
'domain_name' => $domainName,
|
||||
'plan_id' => null,
|
||||
'plan_name' => 'Domain transfer',
|
||||
'amount_minor' => (int) ($quote['amount_minor'] ?? 0),
|
||||
'currency' => $currency,
|
||||
'term_months' => null,
|
||||
'term_years' => 1,
|
||||
'meta' => $meta,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function addServiceRenewal(
|
||||
User $user,
|
||||
RcServiceOrder $existingOrder,
|
||||
int $months,
|
||||
ResellerClubProductService $products
|
||||
): RcServiceOrder {
|
||||
ResellerClubLegacy::assertRenewalsAllowed();
|
||||
|
||||
if ((int) $existingOrder->user_id !== (int) $user->id) {
|
||||
throw new RuntimeException('You can only renew your own services.');
|
||||
}
|
||||
|
||||
if (! filled($existingOrder->rc_order_id)) {
|
||||
throw new RuntimeException('This service cannot be renewed online yet. Please contact support.');
|
||||
}
|
||||
|
||||
if (! $products->supportsCategory($existingOrder->category)) {
|
||||
throw new RuntimeException('Renewals are not available for this product type.');
|
||||
}
|
||||
|
||||
$months = max(1, min(120, $months));
|
||||
$planId = (string) ($existingOrder->plan_id ?? '');
|
||||
|
||||
$quote = $products->quoteRenewal($existingOrder->category, $planId, $months);
|
||||
if (! ($quote['success'] ?? false)) {
|
||||
throw new RuntimeException($quote['message'] ?? 'Renewal pricing is unavailable right now.');
|
||||
}
|
||||
|
||||
$currency = strtoupper((string) ($quote['currency'] ?? 'GHS'));
|
||||
$this->assertCurrencyCompatibility($user, $currency);
|
||||
|
||||
return $this->upsertCartItem(
|
||||
$user,
|
||||
[
|
||||
'category' => $existingOrder->category,
|
||||
'order_type' => RcServiceOrder::TYPE_RENEWAL,
|
||||
'domain_name' => $existingOrder->domain_name,
|
||||
'plan_id' => $planId !== '' ? $planId : null,
|
||||
'plan_name' => (string) ($quote['plan_name'] ?? $existingOrder->plan_name ?? 'Service renewal'),
|
||||
'amount_minor' => (int) ($quote['amount_minor'] ?? 0),
|
||||
'currency' => $currency,
|
||||
'term_months' => $months,
|
||||
'term_years' => null,
|
||||
'meta' => [
|
||||
'is_renewal' => true,
|
||||
'renew_rc_order_id' => (string) $existingOrder->rc_order_id,
|
||||
'renew_source_order_id' => $existingOrder->id,
|
||||
'quote' => $quote,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function addDomainRenewal(
|
||||
User $user,
|
||||
string $domain,
|
||||
int $years,
|
||||
DomainRegistrarService $registrar
|
||||
): RcServiceOrder {
|
||||
ResellerClubLegacy::assertRenewalsAllowed();
|
||||
|
||||
$domainName = strtolower(trim($domain));
|
||||
$years = max(1, min(10, $years));
|
||||
|
||||
$quote = $registrar->quoteDomainRenewal($domainName, $years);
|
||||
if (! ($quote['success'] ?? false)) {
|
||||
throw new RuntimeException($quote['message'] ?? 'Domain renewal pricing is unavailable right now.');
|
||||
}
|
||||
|
||||
$currency = strtoupper((string) ($quote['currency'] ?? 'GHS'));
|
||||
$this->assertCurrencyCompatibility($user, $currency);
|
||||
|
||||
return $this->upsertCartItem(
|
||||
$user,
|
||||
[
|
||||
'category' => 'domain-renewal',
|
||||
'order_type' => RcServiceOrder::TYPE_RENEWAL,
|
||||
'domain_name' => $domainName,
|
||||
'plan_id' => null,
|
||||
'plan_name' => $years === 1 ? '1 year renewal' : "{$years} year renewal",
|
||||
'amount_minor' => (int) ($quote['amount_minor'] ?? 0),
|
||||
'currency' => $currency,
|
||||
'term_months' => null,
|
||||
'term_years' => $years,
|
||||
'meta' => [
|
||||
'is_renewal' => true,
|
||||
'renew_rc_order_id' => (string) ($quote['rc_order_id'] ?? ''),
|
||||
'quote' => $quote,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{items: EloquentCollection<int, RcServiceOrder>, total_minor: int, currency: string, subtotal_minor: int, discount_minor: int, applied_promos: array, promo_name: string|null}
|
||||
*/
|
||||
public function checkoutSummaryForUser(User $user): array
|
||||
{
|
||||
$items = $this->cartItemsForUser($user);
|
||||
ResellerClubLegacy::assertCartCheckoutAllowed($items);
|
||||
|
||||
$currency = strtoupper((string) $items->first()->currency);
|
||||
|
||||
if ($items->contains(fn (RcServiceOrder $item) => strtoupper((string) $item->currency) !== $currency)) {
|
||||
throw new RuntimeException('Your cart contains items in more than one currency. Clear it and try again with a single currency.');
|
||||
}
|
||||
|
||||
$subtotalMinor = (int) $items->sum('amount_minor');
|
||||
|
||||
// Get all active promos with discounts
|
||||
$activePromos = PromoBanner::active()
|
||||
->whereNotNull('discount_percent')
|
||||
->where('discount_percent', '>', 0)
|
||||
->get();
|
||||
|
||||
// Calculate per-item discounts based on product-specific promos
|
||||
$appliedPromos = [];
|
||||
$totalDiscountMinor = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
$itemPromo = $this->findMatchingPromoForItem($item, $activePromos);
|
||||
|
||||
if ($itemPromo) {
|
||||
$discountPercent = (int) $itemPromo->discount_percent;
|
||||
$itemDiscount = (int) round($item->amount_minor * ($discountPercent / 100));
|
||||
$totalDiscountMinor += $itemDiscount;
|
||||
|
||||
// Track applied promos for display
|
||||
$promoKey = $itemPromo->id;
|
||||
if (!isset($appliedPromos[$promoKey])) {
|
||||
$appliedPromos[$promoKey] = [
|
||||
'id' => $itemPromo->id,
|
||||
'name' => $itemPromo->name,
|
||||
'title' => $itemPromo->title ?: $itemPromo->name,
|
||||
'product_type' => $itemPromo->product_type,
|
||||
'discount_percent' => $discountPercent,
|
||||
'discount_minor' => 0,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
$appliedPromos[$promoKey]['discount_minor'] += $itemDiscount;
|
||||
$appliedPromos[$promoKey]['items'][] = [
|
||||
'id' => $item->id,
|
||||
'domain_name' => $item->domain_name,
|
||||
'category' => $item->category,
|
||||
'original_amount' => $item->amount_minor,
|
||||
'discount_amount' => $itemDiscount,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$totalMinor = max(0, $subtotalMinor - $totalDiscountMinor);
|
||||
|
||||
// For backward compatibility, provide a single promo name if only one promo applied
|
||||
$promoName = count($appliedPromos) === 1 ? array_values($appliedPromos)[0]['name'] : null;
|
||||
$discountPercent = count($appliedPromos) === 1 ? array_values($appliedPromos)[0]['discount_percent'] : null;
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'currency' => $currency,
|
||||
'subtotal_minor' => $subtotalMinor,
|
||||
'discount_minor' => $totalDiscountMinor,
|
||||
'discount_percent' => $discountPercent,
|
||||
'promo_name' => $promoName,
|
||||
'applied_promos' => array_values($appliedPromos),
|
||||
'total_minor' => $totalMinor,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best matching promo for a cart item based on product type.
|
||||
*/
|
||||
private function findMatchingPromoForItem(RcServiceOrder $item, $promos): ?PromoBanner
|
||||
{
|
||||
$itemProductType = $this->mapItemToProductType($item);
|
||||
|
||||
// First, try to find a product-specific promo
|
||||
$specificPromo = $promos->first(function (PromoBanner $promo) use ($itemProductType) {
|
||||
return $promo->product_type && $promo->product_type === $itemProductType;
|
||||
});
|
||||
|
||||
if ($specificPromo) {
|
||||
return $specificPromo;
|
||||
}
|
||||
|
||||
// Fall back to general promos (no product_type set or product_type is 'general')
|
||||
return $promos->first(function (PromoBanner $promo) {
|
||||
return !$promo->product_type || $promo->product_type === PromoBanner::TYPE_GENERAL;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a cart item to a promo product type.
|
||||
*/
|
||||
private function mapItemToProductType(RcServiceOrder $item): string
|
||||
{
|
||||
// Domain orders
|
||||
if (in_array($item->order_type, [RcServiceOrder::TYPE_DOMAIN_REGISTRATION, RcServiceOrder::TYPE_DOMAIN_TRANSFER, RcServiceOrder::TYPE_RENEWAL], true)
|
||||
&& str_contains((string) $item->category, 'domain')) {
|
||||
return PromoBanner::TYPE_DOMAINS;
|
||||
}
|
||||
|
||||
// Map category to product type
|
||||
$categoryMap = [
|
||||
'domain-registration' => PromoBanner::TYPE_DOMAINS,
|
||||
'domain-transfer' => PromoBanner::TYPE_DOMAINS,
|
||||
'single-domain-hosting' => PromoBanner::TYPE_HOSTING,
|
||||
'multi-domain-hosting' => PromoBanner::TYPE_HOSTING,
|
||||
'reseller-hosting' => PromoBanner::TYPE_HOSTING,
|
||||
'cloud-hosting' => PromoBanner::TYPE_HOSTING,
|
||||
'vps' => PromoBanner::TYPE_VPS,
|
||||
'dedicated-server' => PromoBanner::TYPE_DEDICATED,
|
||||
'email' => PromoBanner::TYPE_EMAIL,
|
||||
];
|
||||
|
||||
return $categoryMap[$item->category] ?? PromoBanner::TYPE_GENERAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, RcServiceOrder>|EloquentCollection<int, RcServiceOrder> $items
|
||||
*/
|
||||
public function markCheckoutPending(iterable $items, string $reference): void
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
$item->forceFill([
|
||||
'status' => RcServiceOrder::STATUS_PENDING_PAYMENT,
|
||||
'payment_status' => RcServiceOrder::PAYMENT_STATUS_PENDING,
|
||||
'fulfillment_status' => RcServiceOrder::FULFILLMENT_PAYMENT_PENDING,
|
||||
'payment_reference' => $reference,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EloquentCollection<int, RcServiceOrder>
|
||||
*/
|
||||
public function markPaymentSuccessful(array $metadata, string $reference, ?Carbon $paidAt = null): EloquentCollection
|
||||
{
|
||||
$orders = $this->ordersForPaymentMetadata($metadata, $reference);
|
||||
$transitioned = new EloquentCollection();
|
||||
|
||||
foreach ($orders as $order) {
|
||||
if ($order->payment_status === RcServiceOrder::PAYMENT_STATUS_PAID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$order->forceFill([
|
||||
'status' => RcServiceOrder::STATUS_PROCESSING,
|
||||
'payment_status' => RcServiceOrder::PAYMENT_STATUS_PAID,
|
||||
'fulfillment_status' => RcServiceOrder::FULFILLMENT_PAYMENT_RECEIVED,
|
||||
'payment_reference' => $reference,
|
||||
'paid_at' => $paidAt ?: now(),
|
||||
])->save();
|
||||
|
||||
$transitioned->push($order->fresh());
|
||||
}
|
||||
|
||||
return $transitioned;
|
||||
}
|
||||
|
||||
public function markPaymentFailed(array $metadata, string $reference): void
|
||||
{
|
||||
$orders = $this->ordersForPaymentMetadata($metadata, $reference);
|
||||
|
||||
foreach ($orders as $order) {
|
||||
if ($order->payment_status === RcServiceOrder::PAYMENT_STATUS_PAID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$order->forceFill([
|
||||
'status' => RcServiceOrder::STATUS_CART,
|
||||
'payment_status' => RcServiceOrder::PAYMENT_STATUS_FAILED,
|
||||
'fulfillment_status' => RcServiceOrder::FULFILLMENT_CART,
|
||||
'payment_reference' => null,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function returnPendingCheckoutToCart(User $user): int
|
||||
{
|
||||
return RcServiceOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('status', RcServiceOrder::STATUS_PENDING_PAYMENT)
|
||||
->update([
|
||||
'status' => RcServiceOrder::STATUS_CART,
|
||||
'payment_status' => RcServiceOrder::PAYMENT_STATUS_UNPAID,
|
||||
'fulfillment_status' => RcServiceOrder::FULFILLMENT_CART,
|
||||
'payment_reference' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function removeCartItem(User $user, RcServiceOrder $order): void
|
||||
{
|
||||
if ((int) $order->user_id !== (int) $user->id || ! in_array($order->status, $this->cartStatuses(), true)) {
|
||||
throw new RuntimeException('This cart item cannot be removed.');
|
||||
}
|
||||
|
||||
$order->delete();
|
||||
}
|
||||
|
||||
public function clearCart(User $user): int
|
||||
{
|
||||
return RcServiceOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereIn('status', $this->cartStatuses())
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
private function upsertCartItem(User $user, array $attributes): RcServiceOrder
|
||||
{
|
||||
$query = RcServiceOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('order_type', (string) $attributes['order_type'])
|
||||
->where('category', (string) $attributes['category'])
|
||||
->where('domain_name', (string) $attributes['domain_name'])
|
||||
->whereIn('status', $this->cartStatuses());
|
||||
|
||||
if (isset($attributes['plan_id']) && $attributes['plan_id'] !== null) {
|
||||
$query->where('plan_id', (string) $attributes['plan_id']);
|
||||
} else {
|
||||
$query->whereNull('plan_id');
|
||||
}
|
||||
|
||||
if (isset($attributes['term_months']) && $attributes['term_months'] !== null) {
|
||||
$query->where('term_months', (int) $attributes['term_months']);
|
||||
} else {
|
||||
$query->whereNull('term_months');
|
||||
}
|
||||
|
||||
$existing = $query->latest('id')->first();
|
||||
|
||||
$payload = array_merge($attributes, [
|
||||
'user_id' => $user->id,
|
||||
'status' => RcServiceOrder::STATUS_CART,
|
||||
'payment_status' => RcServiceOrder::PAYMENT_STATUS_UNPAID,
|
||||
'fulfillment_status' => RcServiceOrder::FULFILLMENT_CART,
|
||||
'payment_reference' => null,
|
||||
'paid_at' => null,
|
||||
'submitted_at' => null,
|
||||
'last_synced_at' => null,
|
||||
]);
|
||||
|
||||
if ($existing) {
|
||||
$existing->forceFill($payload)->save();
|
||||
|
||||
return $existing->fresh();
|
||||
}
|
||||
|
||||
return RcServiceOrder::query()->create($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $metadata
|
||||
* @return EloquentCollection<int, RcServiceOrder>
|
||||
*/
|
||||
private function ordersForPaymentMetadata(array $metadata, string $reference): EloquentCollection
|
||||
{
|
||||
$ids = collect($metadata['rc_service_order_ids'] ?? [])
|
||||
->map(fn ($value) => (int) $value)
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
if ($ids->isNotEmpty()) {
|
||||
return RcServiceOrder::query()
|
||||
->whereIn('id', $ids->all())
|
||||
->get();
|
||||
}
|
||||
|
||||
return RcServiceOrder::query()
|
||||
->where('payment_reference', $reference)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function assertCurrencyCompatibility(User $user, string $currency): void
|
||||
{
|
||||
$cartCurrency = RcServiceOrder::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereIn('status', $this->cartStatuses())
|
||||
->value('currency');
|
||||
|
||||
if ($cartCurrency && strtoupper((string) $cartCurrency) !== strtoupper($currency)) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'Your cart already contains %s items. Complete or clear that checkout before adding a %s item.',
|
||||
strtoupper((string) $cartCurrency),
|
||||
strtoupper($currency)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user