Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, soap
extensions: dom, curl, libxml, mbstring, zip, soap, intl
coverage: none

- name: Install dependencies
Expand Down
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,80 @@ $url = $user->customerPortalUrl();

Besides the customer portal for managing subscriptions, [Lemon Squeezy also has a "My Orders" portal](https://docs.lemonsqueezy.com/help/online-store/my-orders) to manage all of your purchases for a customer account. This does involve a mixture of purchases across multiple vendors. If this is something you wish your customers can find, you can link to [`https://app.lemonsqueezy.com/my-orders`](https://app.lemonsqueezy.com/my-orders) and tell them to login with the email address they performed the purchase with.

## Orders

Lemon Squeezy allows you to retrieve a list of all orders made for your store. You can then use this list to present all orders to your customers.

### Retrieving Orders

To retrieve a list of orders for a specific customer, simply call the saved models in the database:

```blade
<table>
@foreach ($billable->orders as $order)
<td>{{ $order->ordered_at->toFormattedDateString() }}</td>
<td>{{ $order->order_number }}</td>
<td>{{ $order->subtotal() }}</td>
<td>{{ $order->discount() }}</td>
<td>{{ $order->tax() }}</td>
<td>{{ $order->total() }}</td>
<td>{{ $order->receipt_url }}</td>
@endforeach
</table>
```

### Checking Order Status

To check if an individual order is paid, you may use the `paid` method:

```php
if ($order->paid()) {
// ...
}
```

Besides that, you have three other checks you can do: `pending`, `failed` & `refunded`. If the order is `refunded`, you may also use the `refunded_at` timestamp:

```blade
@if ($order->refunded())
Order {{ $order->order_number }} was rufunded on {{ $order->refunded_at->toFormattedDateString() }}
@endif
```

You can also check if an order was for a specific product:

```php
if ($order->hasProduct('your-product-id')) {
// ...
}
```

Or for a specific variant:

```php
if ($order->hasVariant('your-variant-id')) {
// ...
}
```

Additionally, you may check if a customer has purchased a specific product:

```php
if ($billable->hasPurchasedProduct('your-product-id')) {
// ...
}
```

Or a specific variant:

```php
if ($billable->hasPurchasedVariant('your-variant-id')) {
// ...
}
```

These two checks will both make sure the correct product or variant was purchased and paid for. This is useful as well if you're offering a feature in your app like lifetime access.

## Subscriptions

### Setting Up Subscription Products
Expand Down
14 changes: 14 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Upgrade Guide

Future upgrade notes will be placed here.

## Upgrading To 1.3 From 1.x

### New Order Model

Lemon Squeezy for Laravel v1.3 adds a new `Order` model. In order for your webhooks to start filling these out, you'll need to run the relevant migration:

```shell
php artisan migrate
```

And now your webhooks will start saving newly made orders. If you're overwriting your migrations, you'll need to create [this migration](./database/migrations/2023_01_16_000003_create_orders_table.php) manually.

Previously made orders unfortunately need to be stored manually but we're planning on making a sync command in the future to make this more easily.
13 changes: 13 additions & 0 deletions config/lemon-squeezy.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,17 @@

'redirect_url' => null,

/*
|--------------------------------------------------------------------------
| Currency Locale
|--------------------------------------------------------------------------
|
| This is the default locale in which your money values are formatted in
| for display. To utilize other locales besides the default en locale
| verify you have the "intl" PHP extension installed on the system.
|
*/

'currency_locale' => env('LEMON_SQUEEZY_CURRENCY_LOCALE', 'en'),

];
109 changes: 109 additions & 0 deletions database/factories/OrderFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace LemonSqueezy\Laravel\Database\Factories;

use Carbon\Carbon;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\Factory;
use LemonSqueezy\Laravel\Customer;
use LemonSqueezy\Laravel\Order;

class OrderFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Order::class;

/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'billable_id' => rand(1, 1000),
'billable_type' => 'App\\Models\\User',
'lemon_squeezy_id' => rand(1, 1000),
'customer_id' => rand(1, 1000),
'product_id' => rand(1, 1000),
'variant_id' => rand(1, 1000),
'order_number' => rand(1, 1000),
'currency' => $this->faker->randomElement(['USD', 'EUR', 'GBP']),
'subtotal' => $subtotal = rand(400, 1000),
'discount_total' => $discount = rand(1, 400),
'tax' => $tax = rand(1, 50),
'total' => $subtotal - $discount + $tax,
'tax_name' => $this->faker->randomElement(['VAT', 'Sales Tax']),
'receipt_url' => null,
'ordered_at' => $orderedAt = Carbon::make($this->faker->dateTimeBetween('-1 year', 'now')),
'refunded' => $refunded = $this->faker->boolean(75),
'refunded_at' => $refunded ? $orderedAt->addWeek() : null,
'status' => $refunded ? Order::STATUS_REFUNDED : Order::STATUS_PAID,
];
}

/**
* Configure the model factory.
*/
public function configure(): self
{
return $this->afterCreating(function ($subscription) {
Customer::factory()->create([
'billable_id' => $subscription->billable_id,
'billable_type' => $subscription->billable_type,
]);
});
}

/**
* Mark the order as pending.
*/
public function pending(): self
{
return $this->state([
'status' => Order::STATUS_PENDING,
'refunded' => false,
'refunded_at' => null,
]);
}

/**
* Mark the order as failed.
*/
public function failed(): self
{
return $this->state([
'status' => Order::STATUS_FAILED,
'refunded' => false,
'refunded_at' => null,
]);
}

/**
* Mark the order as paid.
*/
public function paid(): self
{
return $this->state([
'status' => Order::STATUS_PAID,
'refunded' => false,
'refunded_at' => null,
]);
}

/**
* Mark the order as being refunded.
*/
public function refunded(DateTimeInterface $refundedAt = null): self
{
return $this->state([
'status' => Order::STATUS_REFUNDED,
'refunded' => true,
'refunded_at' => $refundedAt,
]);
}
}
42 changes: 42 additions & 0 deletions database/migrations/2023_01_16_000003_create_orders_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::create('lemon_squeezy_orders', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('billable_id');
$table->string('billable_type');
$table->string('lemon_squeezy_id')->unique();
$table->string('customer_id');
$table->uuid('identifier')->unique();
$table->string('product_id');
$table->string('variant_id');
$table->integer('order_number')->unique();
$table->string('currency');
$table->integer('subtotal');
$table->integer('discount_total');
$table->integer('tax');
$table->integer('total');
$table->string('tax_name');
$table->string('status');
$table->string('receipt_url')->nullable();
$table->boolean('refunded');
$table->timestamp('refunded_at')->nullable();
$table->timestamp('ordered_at');
$table->timestamps();

$table->index(['billable_id', 'billable_type']);
});
}

public function down(): void
{
Schema::dropIfExists('lemon_squeezy_orders');
}
};
2 changes: 2 additions & 0 deletions src/Billable.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

use LemonSqueezy\Laravel\Concerns\ManagesCheckouts;
use LemonSqueezy\Laravel\Concerns\ManagesCustomer;
use LemonSqueezy\Laravel\Concerns\ManagesOrders;
use LemonSqueezy\Laravel\Concerns\ManagesSubscriptions;

trait Billable
{
use ManagesCheckouts;
use ManagesCustomer;
use ManagesOrders;
use ManagesSubscriptions;
}
33 changes: 33 additions & 0 deletions src/Concerns/ManagesOrders.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace LemonSqueezy\Laravel\Concerns;

use Illuminate\Database\Eloquent\Relations\MorphMany;
use LemonSqueezy\Laravel\LemonSqueezy;

trait ManagesOrders
{
/**
* Get all of the orders for the billable.
*/
public function orders(): MorphMany
{
return $this->morphMany(LemonSqueezy::$orderModel, 'billable')->orderByDesc('created_at');
}

/**
* Determine if the billable has purchased a specific product.
*/
public function hasPurchasedProduct(string $productId): bool
{
return $this->orders()->where('product_id', $productId)->where('status', static::STATUS_PAID)->exists();
}

/**
* Determine if the billable has purchased a specific variant of a product.
*/
public function hasPurchasedVariant(string $variantId): bool
{
return $this->orders()->where('variant_id', $variantId)->where('status', static::STATUS_PAID)->exists();
}
}
11 changes: 10 additions & 1 deletion src/Events/OrderCreated.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use LemonSqueezy\Laravel\Order;

class OrderCreated
{
Expand All @@ -15,14 +16,22 @@ class OrderCreated
*/
public Model $billable;

/**
* The order entity.
*
* @todo v2: Remove the nullable type hint.
*/
public ?Order $order;

/**
* The payload array.
*/
public array $payload;

public function __construct(Model $billable, array $payload)
public function __construct(Model $billable, ?Order $order, array $payload)
{
$this->billable = $billable;
$this->order = $order;
$this->payload = $payload;
}
}
11 changes: 10 additions & 1 deletion src/Events/OrderRefunded.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use LemonSqueezy\Laravel\Order;

class OrderRefunded
{
Expand All @@ -15,14 +16,22 @@ class OrderRefunded
*/
public Model $billable;

/**
* The order entity.
*
* @todo v2: Remove the nullable type hint.
*/
public ?Order $order;

/**
* The payload array.
*/
public array $payload;

public function __construct(Model $billable, array $payload)
public function __construct(Model $billable, ?Order $order, array $payload)
{
$this->billable = $billable;
$this->order = $order;
$this->payload = $payload;
}
}
Loading