Skip to content

Commit 2a37e3c

Browse files
committed
feat: #104 adjust billing logic to re-imagined pricing
1 parent 931f3b5 commit 2a37e3c

File tree

16 files changed

+487
-265
lines changed

16 files changed

+487
-265
lines changed

.env.example

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,12 @@ PADDLE_CLIENT_SIDE_TOKEN=
6868
PADDLE_API_KEY=
6969
PADDLE_RETAIN_KEY=
7070
PADDLE_WEBHOOK_SECRET=
71-
PADDLE_SERVER_PRICE_ID=pri_01j2ag2ts45hznad1t67bs4syd
71+
72+
PADDLE_PLAN_HOBBY_PRODUCT_ID=pro_01j2ag292f5ntabharerjh94sy
73+
PADDLE_PLAN_HOBBY_PRICE_ID=pri_01j2ag2ts45hznad1t67bs4syd
74+
PADDLE_PLAN_STARTUP_PRODUCT_ID=pro_01j4svv3ys3nh7j6pb0m1hnqq0
75+
PADDLE_PLAN_STARTUP_PRICE_ID=pri_01j4smmphnyk5vafdz95g5hpsn
76+
7277
CASHIER_CURRENCY_LOCALE=en
78+
79+
RESEND_KEY=

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,38 @@
22

33
## About Ptah.sh
44

5-
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.
5+
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.
66

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

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

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

1717
## Ptah.sh Sponsors
1818

19-
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]).
19+
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]).
2020

2121
### Sponsors
2222

23-
- _None so far_
23+
- _None so far_
2424

2525
## Contributing
2626

2727
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).
2828

2929
## Security Vulnerabilities
3030

31-
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.
31+
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.
3232

3333
## License
3434

3535
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).
3636

3737
## Star History ★
3838

39-
[![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)
39+
[![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)

app/Http/Controllers/NodeController.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,21 @@ public function index()
2222
{
2323
$nodes = Node::all();
2424

25-
return Inertia::render('Nodes/Index', ['nodes' => $nodes]);
25+
return Inertia::render('Nodes/Index', [
26+
'nodes' => $nodes,
27+
'nodesLimitReached' => auth()->user()->currentTeam->nodesLimitReached(),
28+
]);
2629
}
2730

2831
/**
2932
* Show the form for creating a new resource.
3033
*/
3134
public function create()
3235
{
36+
if (auth()->user()->currentTeam->nodesLimitReached()) {
37+
return redirect()->route('nodes.index');
38+
}
39+
3340
return Inertia::render('Nodes/Create');
3441
}
3542

@@ -38,6 +45,10 @@ public function create()
3845
*/
3946
public function store(StoreNodeRequest $request)
4047
{
48+
if (auth()->user()->currentTeam->nodesLimitReached()) {
49+
return redirect()->route('nodes.index');
50+
}
51+
4152
$node = Node::make($request->validated());
4253

4354
DB::transaction(function () use ($node) {

app/Http/Controllers/TeamBillingController.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,20 @@ public function show(Team $team, Request $request)
1515
{
1616
$customer = $team->createAsCustomer();
1717

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

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

22+
$plans = config('billing.paddle.plans');
23+
2624
//Cashier::api()
2725
return Inertia::render('Teams/Billing', [
2826
'team' => $team,
2927
'customer' => $customer,
3028
'nextPayment' => $nextPayment,
3129
'subscription' => $subscription?->valid() ? $subscription : null,
32-
'checkout' => $checkout->options(),
3330
'transactions' => $team->transactions,
31+
'plans' => $plans,
3432
'updatePaymentMethodTxnId' => $subscription?->paymentMethodUpdateTransaction()['id'],
3533
'cancelSubscriptionUrl' => $subscription?->cancelUrl(),
3634
]);
@@ -58,14 +56,14 @@ public function updateCustomer(Team $team, Request $request)
5856
return redirect()->route('teams.billing.show', $team);
5957
}
6058

61-
public function subscriptionSuccess(Team $team, Request $request)
59+
public function subscriptionSuccess(Team $team)
6260
{
6361
if (! $team->subscription()?->valid()) {
6462
$team->activating_subscription = true;
6563
$team->save();
6664
}
6765

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

7068
return redirect()->to(route('teams.billing.show', $team));
7169
}

app/Models/Node.php

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Illuminate\Database\Eloquent\Relations\HasMany;
1010
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
1111
use Illuminate\Support\Str;
12+
use RuntimeException;
1213

1314
class Node extends Model
1415
{
@@ -31,49 +32,11 @@ class Node extends Model
3132
protected static function booted(): void
3233
{
3334
self::creating(function (Node $node) {
34-
$node->agent_token = Str::random(42);
35-
});
36-
37-
self::created(function (Node $node) {
38-
$team = $node->team;
39-
40-
$nodesCount = $team->nodes->count();
41-
$subscription = $team->subscription();
42-
43-
if ($subscription->ends_at) {
44-
$subscription->stopCancelation();
45-
}
46-
47-
if ($nodesCount !== 1) {
48-
if ($subscription->onTrial()) {
49-
$subscription->doNotBill()->updateQuantity($nodesCount);
50-
} else {
51-
$subscription->updateQuantity($nodesCount);
52-
}
35+
if ($node->team->nodesLimitReached()) {
36+
throw new RuntimeException('Invalid State - The team is at its node limit');
5337
}
54-
});
55-
56-
self::deleted(function (Node $node) {
57-
$team = $node->team;
5838

59-
$nodesCount = $team->nodes->count();
60-
$subscription = $team->subscription();
61-
62-
if ($subscription->ends_at && $nodesCount > 0) {
63-
$subscription->stopCancelation();
64-
}
65-
66-
if ($nodesCount === 0) {
67-
if (! $subscription->ends_at) {
68-
$subscription->cancel();
69-
}
70-
} else {
71-
if ($subscription->onTrial()) {
72-
$subscription->doNotBill()->updateQuantity($nodesCount);
73-
} else {
74-
$subscription->updateQuantity($nodesCount);
75-
}
76-
}
39+
$node->agent_token = Str::random(42);
7740
});
7841
}
7942

app/Models/PricingPlan.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use App\Models\PricingPlan\UsageQuotas;
6+
use Spatie\LaravelData\Data;
7+
8+
class PricingPlan extends Data
9+
{
10+
public function __construct(
11+
public string $productId,
12+
public UsageQuotas $quotas,
13+
) {}
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace App\Models\PricingPlan;
4+
5+
use Spatie\LaravelData\Data;
6+
7+
class UsageQuotas extends Data
8+
{
9+
public function __construct(
10+
public int $nodes,
11+
) {}
12+
}

app/Models/Team.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Models;
44

5+
use App\Models\PricingPlan\UsageQuotas;
56
use Illuminate\Database\Eloquent\Factories\HasFactory;
67
use Illuminate\Database\Eloquent\Relations\HasMany;
78
use Illuminate\Notifications\Notifiable;
@@ -116,4 +117,24 @@ public function routeNotificationForMail(Notification $notification): array|stri
116117
{
117118
return [$this->customer->email => $this->customer->name];
118119
}
120+
121+
public function quotas(): UsageQuotas
122+
{
123+
if ($this->subscription() === null || ! $this->subscription()->valid()) {
124+
return new UsageQuotas(0);
125+
}
126+
127+
foreach (config('billing.paddle.plans') as $plan) {
128+
if ($this->subscription()->hasProduct($plan['product_id'])) {
129+
return new UsageQuotas($plan['quotas']['nodes']);
130+
}
131+
}
132+
133+
return new UsageQuotas(0);
134+
}
135+
136+
public function nodesLimitReached(): bool
137+
{
138+
return $this->nodes()->count() >= $this->quotas()->nodes;
139+
}
119140
}

config/billing.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22

33
return [
44
'paddle' => [
5-
'server_price_id' => env('PADDLE_SERVER_PRICE_ID'),
5+
'plans' => [
6+
[
7+
'name' => 'Hobby',
8+
'price' => 14,
9+
'description' => 'Perfect plan to try the service or host non-critical applications',
10+
'product_id' => env('PADDLE_PLAN_HOBBY_PRODUCT_ID'),
11+
'price_id' => env('PADDLE_PLAN_HOBBY_PRICE_ID'),
12+
'quotas' => [
13+
'nodes' => 1,
14+
],
15+
],
16+
[
17+
'name' => 'Startup',
18+
'price' => 49,
19+
'description' => 'Need more power or an improved stability? This is your choice!',
20+
'product_id' => env('PADDLE_PLAN_STARTUP_PRODUCT_ID'),
21+
'price_id' => env('PADDLE_PLAN_STARTUP_PRICE_ID'),
22+
'quotas' => [
23+
'nodes' => 9,
24+
],
25+
],
26+
],
627
],
728
];

config/database.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
'search_path' => 'public',
9494
'sslmode' => 'prefer',
9595
'options' => [
96-
PDO::ATTR_EMULATE_PREPARES => (bool) env('DB_EMULATE_PREPARES', false),
96+
PDO::ATTR_PERSISTENT => true,
9797
],
9898
],
9999

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11
<script setup>
22
import PrimaryButton from "@/Components/PrimaryButton.vue";
3+
import { computed } from "vue";
34
45
const props = defineProps({
5-
'checkout': Object,
6+
teamId: Object,
7+
priceId: String,
8+
customerId: String,
9+
name: String,
610
});
711
812
const openCheckout = () => {
9-
Paddle.Checkout.open({
10-
...props.checkout,
11-
settings: {
12-
...props.checkout.settings,
13-
'displayMode': 'overlay',
14-
}
15-
});
16-
}
13+
const checkout = {
14+
settings: {
15+
displayMode: "overlay",
16+
successUrl: route(
17+
"teams.billing.subscription-success",
18+
{ team: props.teamId },
19+
true,
20+
),
21+
},
22+
items: [{ priceId: props.priceId, quantity: 1 }],
23+
customer: { id: props.customerId },
24+
};
25+
26+
Paddle.Checkout.open(checkout);
27+
};
1728
</script>
1829

1930
<template>
20-
<PrimaryButton class="paddle_button bg-green-500 hover:bg-green-700"
21-
type="button"
22-
@click="openCheckout()"
23-
>Start Free Trial - 14 days</PrimaryButton>
24-
</template>
31+
<PrimaryButton
32+
class="paddle_button bg-green-500 hover:bg-green-700"
33+
type="button"
34+
@click="openCheckout()"
35+
><span class="w-full text-center"
36+
>{{ name }} - Start Free Trial</span
37+
></PrimaryButton
38+
>
39+
</template>

0 commit comments

Comments
 (0)