Working on v3

This commit is contained in:
David Bomba 2024-11-14 16:23:17 +11:00
parent 8b8d20837f
commit 4837bd210e
15 changed files with 172 additions and 108 deletions

View File

@ -77,7 +77,6 @@ class SubscriptionPurchaseController extends Controller
public function v3(Subscription $subscription, Request $request)
{
// Todo: Prerequirement checks for subscription.
return view('billing-portal.v3.index', [
'subscription' => $subscription,

View File

@ -88,7 +88,7 @@ class Login extends Component
auth()->guard('contact')->loginUsingId($contact->id, true);
$this->dispatch('purchase.context', property: 'contact', value: $contact);
// $this->dispatch('purchase.context', property: 'contact', value: $contact);
$this->dispatch('purchase.next');
}
@ -96,6 +96,7 @@ class Login extends Component
{
$code = rand(100000, 999999);
$email_hash = "subscriptions:otp:{$this->email}";
$contact = auth()->guard('contact')->user();
Cache::put($email_hash, $code, 600);
@ -103,7 +104,7 @@ class Login extends Component
$cc->email = $this->email;
$nmo = new NinjaMailerObject();
$nmo->mailable = new OtpCode($this->subscription()->company, $this->context['contact'] ?? null, $code);
$nmo->mailable = new OtpCode($this->subscription()->company, $contact ?? null, $code);
$nmo->company = $this->subscription()->company;
$nmo->settings = $this->subscription()->company->settings;
$nmo->to_user = $cc;
@ -140,7 +141,7 @@ class Login extends Component
if ($contact) {
auth()->guard('contact')->loginUsingId($contact->id, true);
$this->dispatch('purchase.context', property: 'contact', value: $contact);
// $this->dispatch('purchase.context', property: 'contact', value: $contact);
$this->dispatch('purchase.next');
return;
@ -162,7 +163,7 @@ class Login extends Component
if ($attempt) {
$this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
// $this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
$this->dispatch('purchase.next');
}
@ -172,7 +173,7 @@ class Login extends Component
public function mount()
{
if (auth()->guard('contact')->check()) {
$this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
// $this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
$this->dispatch('purchase.next');
}

View File

@ -92,7 +92,7 @@ class Register extends Component
auth()->guard('contact')->loginUsingId($contact->id, true);
$this->dispatch('purchase.context', property: 'contact', value: $contact);
// $this->dispatch('purchase.context', property: 'contact', value: $contact);
$this->dispatch('purchase.next');
}
@ -112,7 +112,7 @@ class Register extends Component
auth()->guard('contact')->loginUsingId($contact->id, true);
$this->dispatch('purchase.context', property: 'contact', value: $contact);
// $this->dispatch('purchase.context', property: 'contact', value: $contact);
$this->dispatch('purchase.next');
return;
@ -161,7 +161,7 @@ class Register extends Component
public function mount()
{
if (auth()->guard('contact')->check()) {
$this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
// $this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
$this->dispatch('purchase.next');
}

View File

@ -125,10 +125,10 @@ class RegisterOrLogin extends Component
$cc->email = $this->email;
$nmo = new NinjaMailerObject();
$nmo->mailable = new OtpCode($this->subscription()->company, $this->context['contact'] ?? null, $code);
$nmo->mailable = new OtpCode($this->subscription()->company, $contact, $code);
$nmo->company = $this->subscription()->company;
$nmo->settings = $this->subscription()->company->settings;
$nmo->to_user = $cc;
$nmo->to_user = $contact;
NinjaMailerJob::dispatch($nmo);
@ -161,7 +161,7 @@ class RegisterOrLogin extends Component
if ($contact) {
auth()->guard('contact')->loginUsingId($contact->id, true);
$this->dispatch('purchase.context', property: 'contact', value: $contact);
// $this->dispatch('purchase.context', property: 'contact', value: $contact);
$this->dispatch('purchase.next');
return;
@ -184,8 +184,6 @@ class RegisterOrLogin extends Component
additional: $this->additional_fields,
);
nlog($data);
$rules = $service->rules();
$data = Validator::make($data, $rules)->validate();
@ -194,7 +192,7 @@ class RegisterOrLogin extends Component
auth()->guard('contact')->loginUsingId($contact->id, true);
$this->dispatch('purchase.context', property: 'contact', value: $contact);
// $this->dispatch('purchase.context', property: 'contact', value: $contact);
$this->dispatch('purchase.next');
}
@ -214,7 +212,7 @@ class RegisterOrLogin extends Component
auth()->guard('contact')->loginUsingId($contact->id, true);
$this->dispatch('purchase.context', property: 'contact', value: $contact);
// $this->dispatch('purchase.context', property: 'contact', value: $contact);
$this->dispatch('purchase.next');
return;
@ -264,7 +262,7 @@ class RegisterOrLogin extends Component
{
if (auth()->guard('contact')->check()) {
$this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
// $this->dispatch('purchase.context', property: 'contact', value: auth()->guard('contact')->user());
$this->dispatch('purchase.next');
return;

View File

@ -27,6 +27,20 @@ class Coupon extends Component
public ?string $couponCode = null;
public bool $showCouponCode = false;
public function mount()
{
$subscription = $this->subscription();
$this->showCouponCode = ($subscription->promo_discount > 0) && (!array_key_exists('valid_coupon',$this->context));
if(isset($this->context['request_data']['coupon']) && $this->context['request_data']['coupon'] == $this->subscription()->promo_code){
$this->showCouponCode = false;
$this->dispatch('purchase.context', property: "valid_coupon", value: $this->context['request_data']['coupon']);
}
}
#[Computed()]
public function subscription()
{
@ -35,32 +49,28 @@ class Coupon extends Component
public function applyCoupon()
{
$this->validate([
'couponCode' => ['required', 'string', 'min:3'],
]);
try {
if($this->couponCode == $this->subscription()->promo_code) {
$this->couponCode = null;
$this->showCouponCode = false;
$this->dispatch('purchase.context', property: "valid_coupon", value: $this->couponCode);
$this->dispatch('summary.refresh');
}
else {
$this->addError('couponCode', 'Invalid coupon code.');
$this->addError('couponCode', ctrans('texts.invalid_coupon'));
}
} catch (\Exception $e) {
$this->addError('couponCode', 'Invalid coupon code.');
$this->addError('couponCode', ctrans('texts.invalid_coupon'));
}
}
// public function quantity($id, $value): void
// {
// $this->dispatch('purchase.context', property: "bundle.optional_one_time_products.{$id}.quantity", value: $value);
// }
}
public function render(): \Illuminate\View\View
{

View File

@ -29,17 +29,12 @@ class Methods extends Component
public function mount(): void
{
nlog($this->subscription_id);
$total = collect($this->context['products'])->sum('total_raw');
$methods = auth()->guard('contact')->user()->client->service()->getPaymentMethods($total); //@todo this breaks down when the cart is in front of the login - we have no context on the user - nor their country/currency()
$this->methods = $methods;
nlog($methods);
}
public function handleSelect(string $company_gateway_id, string $gateway_type_id)

View File

@ -82,63 +82,99 @@ class Summary extends Component
}
$this->dispatch('purchase.context', property: 'bundle', value: $bundle);
}
public function oneTimePurchasesTotal(bool $raw = false)
/**
* Base calculations for one-time purchases
*/
#[Computed]
public function oneTimePurchasesTotal(): float
{
if (isset($this->context['bundle']['recurring_products']) === false) {
return 0;
if (!isset($this->context['bundle']['one_time_products'])) {
return 0.0;
}
$one_time = collect($this->context['bundle']['one_time_products'])->sum(function ($item) {
return $item['product']['price'] * $item['quantity'];
return (float)$item['product']['price'] * (float)$item['quantity'];
});
$one_time_optional = collect($this->context['bundle']['optional_one_time_products'])->sum(function ($item) {
return $item['product']['price'] * $item['quantity'];
return (float)$item['product']['price'] * (float)$item['quantity'];
});
if ($raw) {
return $one_time + $one_time_optional;
}
return Number::formatMoney($one_time + $one_time_optional, $this->subscription()->company);
return (float)$one_time + (float)$one_time_optional;
}
public function recurringPurchasesTotal(bool $raw = false)
/**
* Base calculations for recurring purchases
*/
#[Computed]
public function recurringPurchasesTotal(): float
{
if (isset($this->context['bundle']['recurring_products']) === false) {
return 0;
if (!isset($this->context['bundle']['recurring_products'])) {
return 0.0;
}
$recurring = collect($this->context['bundle']['recurring_products'])->sum(function ($item) {
return $item['product']['price'] * $item['quantity'];
return (float)$item['product']['price'] * (float)$item['quantity'];
});
$recurring_optional = collect($this->context['bundle']['optional_recurring_products'])->sum(function ($item) {
return $item['product']['price'] * $item['quantity'];
return (float)$item['product']['price'] * (float)$item['quantity'];
});
if ($raw) {
return $recurring + $recurring_optional;
return (float)$recurring + (float)$recurring_optional;
}
/**
* Calculate subtotal before any discounts
*/
#[Computed]
protected function calculateSubtotal(): float
{
return $this->oneTimePurchasesTotal() + $this->recurringPurchasesTotal();
}
/**
* Calculate discount amount based on subtotal
*/
#[Computed]
public function discount(): float
{
if (!isset($this->context['valid_coupon']) ||
$this->context['valid_coupon'] != $this->subscription()->promo_code) {
return 0.0;
}
return \sprintf(
'%s/%s',
Number::formatMoney($recurring + $recurring_optional, $this->subscription()->company),
RecurringInvoice::frequencyForKey($this->subscription()->frequency_id ?? 0)
$subscription = $this->subscription();
$discount = $subscription->promo_discount;
return $subscription->is_amount_discount
? $discount
: ($this->calculateSubtotal() * $discount/100);
}
/**
* Format subtotal for display
*/
#[Computed]
public function subtotal(): string
{
return Number::formatMoney(
$this->calculateSubtotal(),
$this->subscription()->company
);
}
#[Computed()]
public function total()
/**
* Calculate and format final total
*/
#[Computed]
public function total(): string
{
return Number::formatMoney(
collect([
$this->oneTimePurchasesTotal(raw: true),
$this->recurringPurchasesTotal(raw: true),
])->sum(),
$this->calculateSubtotal() - $this->discount(),
$this->subscription()->company
);
}
@ -192,6 +228,23 @@ class Summary extends Component
return $products;
}
#[On('summary.refresh')]
public function refresh()
{
// nlog("am i refreshing here?");
// $this->oneTimePurchasesTotal = $this->oneTimePurchasesTotal();
// $this->recurringPurchasesTotal = $this->recurringPurchasesTotal();
// $this->discount = $this->discount();
// nlog($this->oneTimePurchasesTotal);
// nlog($this->recurringPurchasesTotal);
// nlog($this->discount);
}
public function render()
{
return view('billing-portal.v3.summary');

View File

@ -214,10 +214,14 @@ class Subscription extends BaseModel
$in_stock_quantity = data_get($product, 'in_stock_quantity', 0);
$max_limit = 100;
if(!$this->use_inventory_management)
return $max_quantity != 0 ? $max_quantity : $max_limit;
if (!$this->use_inventory_management) {
return min($max_limit, $max_quantity > 0 ? $max_quantity : $max_limit);
}
return $max_quantity !=0 ? $max_quantity : min($max_limit, $in_stock_quantity);
if ($max_quantity > 0) {
return min($max_limit, $max_quantity);
}
return min($max_limit, $in_stock_quantity);
}
}

View File

@ -229,7 +229,6 @@ class UpdatePaymentMethods
private function buildPaymentMethodMeta(PaymentMethod $method, $type_id)
{
nlog($method->type);
switch ($type_id) {
case GatewayType::CREDIT_CARD:

View File

@ -14,6 +14,7 @@
@endif
<livewire:billing-portal.cart.optional-recurring-products
:subscription_id="$this->subscription->hashed_id"
:context="$context"
/>
@ -24,12 +25,10 @@
:context="$context"
/>
@if($this->subscription->promo_discount > 0)
<livewire:billing-portal.cart.coupon
:subscription_id="$this->subscription->hashed_id"
:context="$context"
/>
@endif
<div class="mt-3">
<form wire:submit="handleSubmit">

View File

@ -1,11 +1,12 @@
<div class="space-y-10">
@if($showCouponCode)
<div>
<div class="flex rounded-lg overflow-hidden border border-gray-300">
<div class="relative flex-grow">
<input
type="text"
wire:model.live="couponCode"
placeholder="Enter coupon code"
wire:model="couponCode"
placeholder="{{ __('texts.promo_code') }}"
class="block w-full px-4 py-2 border-0 outline-none focus:ring-0 sm:text-sm"
>
</div>
@ -14,7 +15,7 @@
wire:loading.attr="disabled"
class="inline-flex items-center border-l border-gray-300 bg-gray-50 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<span wire:loading.remove wire:target="applyCoupon">Apply</span>
<span wire:loading.remove wire:target="applyCoupon">{{ __('texts.apply') }}</span>
<span wire:loading wire:target="applyCoupon">
<svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
@ -27,4 +28,5 @@
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
@endif
</div>

View File

@ -29,15 +29,15 @@
<div class="flex flex-col-reverse space-y-3">
<div class="flex">
@if($subscription->use_inventory_management && $product['in_stock_quantity'] <= 0)
@if($this->subscription->use_inventory_management && $product['in_stock_quantity'] <= 0)
<p class="text-sm font-light text-red-500 text-right mr-2 mt-2">{{ ctrans('texts.out_of_stock') }}</p>
@else
<p class="text-sm font-light text-gray-700 text-right mr-2 mt-2">{{ ctrans('texts.qty') }}</p>
@endif
<select id="{{ $product['hashed_id'] }}" wire:change="quantity($event.target.id, $event.target.value)" class="rounded-md border-gray-300 shadow-sm sm:text-sm" {{ $subscription->use_inventory_management && $product['in_stock_quantity'] < 1 ? 'disabled' : '' }}>
<select id="{{ $product['hashed_id'] }}" wire:change="quantity($event.target.id, $event.target.value)" class="rounded-md border-gray-300 shadow-sm sm:text-sm" {{ $this->subscription->use_inventory_management && $product['in_stock_quantity'] < 1 ? 'disabled' : '' }}>
<option {{ $entry['quantity'] == '0' ? 'selected' : '' }} value="0" selected="selected">0</option>
@for ($i = 1; $i <= $subscription->maxQuantity($product); $i++)
@for ($i = 1; $i <= $this->subscription->maxQuantity($product); $i++)
<option {{ $entry['quantity'] == $i ? 'selected' : '' }} value="{{ $i }}">{{ $i }}</option>
@endfor
</select>

View File

@ -23,22 +23,22 @@
<div class="flex flex-col">
<h2 class="text-lg font-medium">{{ $product['product_key'] }}</h2>
<p class="block text-sm">{{ \App\Utils\Number::formatMoney($product['price'], $this->subscription['company']) }} / <span class="lowercase">{{ App\Models\RecurringInvoice::frequencyForKey($subscription->frequency_id) }}</span></p>
<p class="block text-sm">{{ \App\Utils\Number::formatMoney($product['price'], $this->subscription['company']) }} / <span class="lowercase">{{ App\Models\RecurringInvoice::frequencyForKey($this->subscription->frequency_id) }}</span></p>
</div>
</div>
<div class="flex flex-col-reverse space-y-3">
<div class="flex">
@if($subscription->use_inventory_management && $product['in_stock_quantity'] <= 0)
@if($this->subscription->use_inventory_management && $product['in_stock_quantity'] <= 0)
<p class="text-sm font-light text-red-500 text-right mr-2 mt-2">{{ ctrans('texts.out_of_stock') }}</p>
@else
<p class="text-sm font-light text-gray-700 text-right mr-2 mt-2">{{ ctrans('texts.qty') }}</p>
@endif
<select id="{{ $product['hashed_id'] }}" wire:change="quantity($event.target.id, $event.target.value)" class="rounded-md border-gray-300 shadow-sm sm:text-sm" {{ $subscription->use_inventory_management && $product['in_stock_quantity'] < 1 ? 'disabled' : '' }}>
<select id="{{ $product['hashed_id'] }}" wire:change="quantity($event.target.id, $event.target.value)" class="rounded-md border-gray-300 shadow-sm sm:text-sm" {{ $this->subscription->use_inventory_management && $product['in_stock_quantity'] < 1 ? 'disabled' : '' }}>
<option {{ $entry['quantity'] == '0' ? 'selected' : '' }} value="0" selected="selected">0</option>
@for ($i = 1; $i <= $subscription->maxQuantity($product); $i++)
@for ($i = 1; $i <= $this->subscription->maxQuantity($product); $i++)
<option {{ $entry['quantity'] == $i ? 'selected' : '' }} value="{{ $i }}">{{ $i }}</option>
@endfor
</select>

View File

@ -23,14 +23,14 @@
<div class="flex flex-col">
<h2 class="text-lg font-medium">{{ $product['product_key'] }}</h2>
<p class="block text-sm">{{ \App\Utils\Number::formatMoney($product['price'], $this->subscription['company']) }} / <span class="lowercase">{{ App\Models\RecurringInvoice::frequencyForKey($subscription->frequency_id) }}</span></p>
<p class="block text-sm">{{ \App\Utils\Number::formatMoney($product['price'], $this->subscription['company']) }} / <span class="lowercase">{{ App\Models\RecurringInvoice::frequencyForKey($this->subscription->frequency_id) }}</span></p>
</div>
</div>
<div class="flex flex-col-reverse space-y-3">
<div class="flex">
@if($subscription->per_seat_enabled)
@if($subscription->use_inventory_management && $product['in_stock_quantity'] < 1)
@if($this->subscription->per_seat_enabled)
@if($this->subscription->use_inventory_management && $product['in_stock_quantity'] < 1)
<p class="text-sm font-light text-red-500 text-right mr-2 mt-2">{{ ctrans('texts.out_of_stock') }}</p>
@else
<p class="text-sm font-light text-gray-700 text-right mr-2 mt-2">{{ ctrans('texts.qty') }}</p>
@ -40,19 +40,13 @@
id="{{ $product['hashed_id'] }}"
class="rounded-md border-gray-300 shadow-sm sm:text-sm"
wire:change="quantity($event.target.id, $event.target.value)"
{{ $subscription->use_inventory_management && $product['in_stock_quantity'] < 1 ? 'disabled' : '' }}
{{ $this->subscription->use_inventory_management && $product['in_stock_quantity'] < 1 ? 'disabled' : '' }}
>
<option {{ $entry['quantity'] == '1' ? 'selected' : '' }} value="1">1</option>
@if($subscription->max_seats_limit > 1)
@for ($i = 2; $i <= ($subscription->use_inventory_management ? min($this->subscription->max_seats_limit,$product['in_stock_quantity']) : $subscription->max_seats_limit); $i++)
<option {{ $entry['quantity'] == $i ? 'selected' : '' }} value="{{ $i }}">{{ $i }}</option>
@endfor
@else
@for ($i = 2; $i <= ($subscription->use_inventory_management ? min($product['in_stock_quantity'], min(100,$product['max_quantity'])) : min(100,$product['max_quantity'])); $i++)
<option {{ $entry['quantity'] == $i ? 'selected' : '' }} value="{{ $i }}">{{ $i }}</option>
@endfor
@endif
@for ($i = 1; $i <= $this->subscription->maxQuantity($product); $i++)
<option {{ $entry['quantity'] == $i ? 'selected' : '' }} value="{{ $i }}">{{ $i }}</option>
@endfor
</select>
@endif
</div>

View File

@ -13,23 +13,33 @@
@endforeach
</div>
<div class="space-y-2 mt-4 border-t pt-2">
<div class="flex justify-between text-sm">
<span class="uppercase">{{ ctrans('texts.one_time_purchases') }}</span>
<span>{{ $this->oneTimePurchasesTotal() }}</span>
</div>
<div class="space-y-2 mt-4 border-t pt-2">
<div class="flex justify-between text-sm">
<span class="uppercase">{{ ctrans('texts.one_time_purchases') }}</span>
<span>{{ \App\Utils\Number::formatMoney($this->oneTimePurchasesTotal(), $this->subscription->company) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="uppercase">{{ ctrans('texts.recurring_purchases') }}</span>
<span>{{ $this->recurringPurchasesTotal() }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="uppercase">{{ ctrans('texts.recurring_purchases') }}</span>
<span>{{ \App\Utils\Number::formatMoney($this->recurringPurchasesTotal(), $this->subscription->company) }}</span>
</div>
<div
class="flex justify-between text-sm uppercase border-t pt-2"
>
<span>{{ ctrans('texts.total') }}</span>
<span class="font-semibold">{{ $this->total() }}</span>
<div class="flex justify-between text-sm uppercase border-t pt-2">
<span>{{ ctrans('texts.subtotal') }}</span>
<span>{{ $this->subtotal() }}</span>
</div>
@if($this->discount() > 0)
<div class="flex justify-between text-sm uppercase">
<span>{{ ctrans('texts.discount') }}</span>
<span class="font-semibold">{{ \App\Utils\Number::formatMoney($this->discount(), $this->subscription->company) }}</span>
</div>
@endif
<div class="flex justify-between text-sm uppercase border-t pt-2">
<span>{{ ctrans('texts.total') }}</span>
<span class="font-semibold">{{ $this->total() }}</span>
</div>
</div>
</div>
@endif
</div>