Skip to content

Commit

Permalink
feat: #104 adjust billing logic to re-imagined pricing
Browse files Browse the repository at this point in the history
  • Loading branch information
bohdan-shulha committed Aug 8, 2024
1 parent 931f3b5 commit 2a37e3c
Show file tree
Hide file tree
Showing 16 changed files with 487 additions and 265 deletions.
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,12 @@ PADDLE_CLIENT_SIDE_TOKEN=
PADDLE_API_KEY=
PADDLE_RETAIN_KEY=
PADDLE_WEBHOOK_SECRET=
PADDLE_SERVER_PRICE_ID=pri_01j2ag2ts45hznad1t67bs4syd

PADDLE_PLAN_HOBBY_PRODUCT_ID=pro_01j2ag292f5ntabharerjh94sy
PADDLE_PLAN_HOBBY_PRICE_ID=pri_01j2ag2ts45hznad1t67bs4syd
PADDLE_PLAN_STARTUP_PRODUCT_ID=pro_01j4svv3ys3nh7j6pb0m1hnqq0
PADDLE_PLAN_STARTUP_PRICE_ID=pri_01j4smmphnyk5vafdz95g5hpsn

CASHIER_CURRENCY_LOCALE=en

RESEND_KEY=
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,38 @@

## About Ptah.sh

Ptah.sh is an open-source self-hosting deployment platform - alternative to Heroku/Vercel and other Big Corp software. We believe that indie, startups and small to medium businesses must not suffer from unpredicted billing or bare-metal/VPS configurations.
Ptah.sh is an open-source self-hosting deployment platform - alternative to Heroku/Vercel and other Big Corp software. We believe that indie, startups and small to medium businesses must not suffer from unpredicted billing or bare-metal/VPS configurations.

The service is built on top of the proven container management solution - Docker Swarm.

Ptah.sh takes the pain out of deployment by easing common tasks used in many projects, such as:

- Setting up stateful services (PostgreSQL, MongoDB, MySQL and others).
- Scaling stateless services to an infinite number of nodes (servers, as much as Docker Swarm can do).
- Managing automated backups for critical data.
- Load balancing of an incoming traffic and SSL auto-provisioning via Caddy Server.
- And many more features.
- Setting up stateful services (PostgreSQL, MongoDB, MySQL and others).
- Scaling stateless services to an infinite number of nodes (servers, as much as Docker Swarm can do).
- Managing automated backups for critical data.
- Load balancing of an incoming traffic and SSL auto-provisioning via Caddy Server.
- And many more features.

## Ptah.sh Sponsors

We would like to extend our thanks to the following sponsors for funding Ptah.sh development. If you are interested in becoming a sponsor, please send an e-mail to Bohdan Shulha via [[email protected]](mailto:[email protected]).
We would like to extend our thanks to the following sponsors for funding Ptah.sh development. If you are interested in becoming a sponsor, please send an e-mail to Bohdan Shulha via [[email protected]](mailto:[email protected]).

### Sponsors

- _None so far_
- _None so far_

## Contributing

Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [CONTRIBUTING.md](https://github.com/ptah-sh/ptah-server/blob/main/CONTRIBUTING.md).

## Security Vulnerabilities

If you discover a security vulnerability within Ptah.sh services, please send an e-mail to Bohdan Shulha via [[email protected]](mailto:[email protected]). All security vulnerabilities will be promptly addressed.
If you discover a security vulnerability within Ptah.sh services, please send an e-mail to Bohdan Shulha via [[email protected]](mailto:[email protected]). All security vulnerabilities will be promptly addressed.

## License

The Ptah.sh service suite is open-sourced software licensed under the [Functional Source License, Version 1.1, Apache 2.0 Future License](https://github.com/ptah-sh/ptah-server/blob/main/LICENSE.md).

## Star History ★

[![Star History Chart](https://api.star-history.com/svg?repos=ptah-sh/ptah-server&type=Date)](https://star-history.com/#ptah-sh/ptah-server&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=ptah-sh/ptah-server&type=Date)](https://star-history.com/#ptah-sh/ptah-server&Date)
13 changes: 12 additions & 1 deletion app/Http/Controllers/NodeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@ public function index()
{
$nodes = Node::all();

return Inertia::render('Nodes/Index', ['nodes' => $nodes]);
return Inertia::render('Nodes/Index', [
'nodes' => $nodes,
'nodesLimitReached' => auth()->user()->currentTeam->nodesLimitReached(),
]);
}

/**
* Show the form for creating a new resource.
*/
public function create()
{
if (auth()->user()->currentTeam->nodesLimitReached()) {
return redirect()->route('nodes.index');
}

return Inertia::render('Nodes/Create');
}

Expand All @@ -38,6 +45,10 @@ public function create()
*/
public function store(StoreNodeRequest $request)
{
if (auth()->user()->currentTeam->nodesLimitReached()) {
return redirect()->route('nodes.index');
}

$node = Node::make($request->validated());

DB::transaction(function () use ($node) {
Expand Down
12 changes: 5 additions & 7 deletions app/Http/Controllers/TeamBillingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,20 @@ public function show(Team $team, Request $request)
{
$customer = $team->createAsCustomer();

$checkout = $team->subscribe(config('billing.paddle.server_price_id'))->customData([
'team_id' => $team->id,
])->returnTo(route('teams.billing.subscription-success', $team));

$subscription = $team->subscription();

$nextPayment = $subscription?->nextPayment();

$plans = config('billing.paddle.plans');

//Cashier::api()
return Inertia::render('Teams/Billing', [
'team' => $team,
'customer' => $customer,
'nextPayment' => $nextPayment,
'subscription' => $subscription?->valid() ? $subscription : null,
'checkout' => $checkout->options(),
'transactions' => $team->transactions,
'plans' => $plans,
'updatePaymentMethodTxnId' => $subscription?->paymentMethodUpdateTransaction()['id'],
'cancelSubscriptionUrl' => $subscription?->cancelUrl(),
]);
Expand Down Expand Up @@ -58,14 +56,14 @@ public function updateCustomer(Team $team, Request $request)
return redirect()->route('teams.billing.show', $team);
}

public function subscriptionSuccess(Team $team, Request $request)
public function subscriptionSuccess(Team $team)
{
if (! $team->subscription()?->valid()) {
$team->activating_subscription = true;
$team->save();
}

session()->flash('success', "Payment successfully processed. We'll active your subscription in a few minutes.");
session()->flash('success', "Payment successfully processed. We'll activate your subscription in a few minutes.");

return redirect()->to(route('teams.billing.show', $team));
}
Expand Down
45 changes: 4 additions & 41 deletions app/Models/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Support\Str;
use RuntimeException;

class Node extends Model
{
Expand All @@ -31,49 +32,11 @@ class Node extends Model
protected static function booted(): void
{
self::creating(function (Node $node) {
$node->agent_token = Str::random(42);
});

self::created(function (Node $node) {
$team = $node->team;

$nodesCount = $team->nodes->count();
$subscription = $team->subscription();

if ($subscription->ends_at) {
$subscription->stopCancelation();
}

if ($nodesCount !== 1) {
if ($subscription->onTrial()) {
$subscription->doNotBill()->updateQuantity($nodesCount);
} else {
$subscription->updateQuantity($nodesCount);
}
if ($node->team->nodesLimitReached()) {
throw new RuntimeException('Invalid State - The team is at its node limit');
}
});

self::deleted(function (Node $node) {
$team = $node->team;

$nodesCount = $team->nodes->count();
$subscription = $team->subscription();

if ($subscription->ends_at && $nodesCount > 0) {
$subscription->stopCancelation();
}

if ($nodesCount === 0) {
if (! $subscription->ends_at) {
$subscription->cancel();
}
} else {
if ($subscription->onTrial()) {
$subscription->doNotBill()->updateQuantity($nodesCount);
} else {
$subscription->updateQuantity($nodesCount);
}
}
$node->agent_token = Str::random(42);
});
}

Expand Down
14 changes: 14 additions & 0 deletions app/Models/PricingPlan.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace App\Models;

use App\Models\PricingPlan\UsageQuotas;
use Spatie\LaravelData\Data;

class PricingPlan extends Data
{
public function __construct(
public string $productId,
public UsageQuotas $quotas,
) {}
}
12 changes: 12 additions & 0 deletions app/Models/PricingPlan/UsageQuotas.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace App\Models\PricingPlan;

use Spatie\LaravelData\Data;

class UsageQuotas extends Data
{
public function __construct(
public int $nodes,
) {}
}
21 changes: 21 additions & 0 deletions app/Models/Team.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Models;

use App\Models\PricingPlan\UsageQuotas;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Notifications\Notifiable;
Expand Down Expand Up @@ -116,4 +117,24 @@ public function routeNotificationForMail(Notification $notification): array|stri
{
return [$this->customer->email => $this->customer->name];
}

public function quotas(): UsageQuotas
{
if ($this->subscription() === null || ! $this->subscription()->valid()) {
return new UsageQuotas(0);
}

foreach (config('billing.paddle.plans') as $plan) {
if ($this->subscription()->hasProduct($plan['product_id'])) {
return new UsageQuotas($plan['quotas']['nodes']);
}
}

return new UsageQuotas(0);
}

public function nodesLimitReached(): bool
{
return $this->nodes()->count() >= $this->quotas()->nodes;
}
}
23 changes: 22 additions & 1 deletion config/billing.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

return [
'paddle' => [
'server_price_id' => env('PADDLE_SERVER_PRICE_ID'),
'plans' => [
[
'name' => 'Hobby',
'price' => 14,
'description' => 'Perfect plan to try the service or host non-critical applications',
'product_id' => env('PADDLE_PLAN_HOBBY_PRODUCT_ID'),
'price_id' => env('PADDLE_PLAN_HOBBY_PRICE_ID'),
'quotas' => [
'nodes' => 1,
],
],
[
'name' => 'Startup',
'price' => 49,
'description' => 'Need more power or an improved stability? This is your choice!',
'product_id' => env('PADDLE_PLAN_STARTUP_PRODUCT_ID'),
'price_id' => env('PADDLE_PLAN_STARTUP_PRICE_ID'),
'quotas' => [
'nodes' => 9,
],
],
],
],
];
2 changes: 1 addition & 1 deletion config/database.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
'search_path' => 'public',
'sslmode' => 'prefer',
'options' => [
PDO::ATTR_EMULATE_PREPARES => (bool) env('DB_EMULATE_PREPARES', false),
PDO::ATTR_PERSISTENT => true,
],
],

Expand Down
43 changes: 29 additions & 14 deletions resources/js/Components/PaddleButton.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
<script setup>
import PrimaryButton from "@/Components/PrimaryButton.vue";
import { computed } from "vue";
const props = defineProps({
'checkout': Object,
teamId: Object,
priceId: String,
customerId: String,
name: String,
});
const openCheckout = () => {
Paddle.Checkout.open({
...props.checkout,
settings: {
...props.checkout.settings,
'displayMode': 'overlay',
}
});
}
const checkout = {
settings: {
displayMode: "overlay",
successUrl: route(
"teams.billing.subscription-success",
{ team: props.teamId },
true,
),
},
items: [{ priceId: props.priceId, quantity: 1 }],
customer: { id: props.customerId },
};
Paddle.Checkout.open(checkout);
};
</script>

<template>
<PrimaryButton class="paddle_button bg-green-500 hover:bg-green-700"
type="button"
@click="openCheckout()"
>Start Free Trial - 14 days</PrimaryButton>
</template>
<PrimaryButton
class="paddle_button bg-green-500 hover:bg-green-700"
type="button"
@click="openCheckout()"
><span class="w-full text-center"
>{{ name }} - Start Free Trial</span
></PrimaryButton
>
</template>
Loading

0 comments on commit 2a37e3c

Please sign in to comment.