Skip to content

Commit

Permalink
Implement Orders (#60)
Browse files Browse the repository at this point in the history
* Implement support for Orders

* Upgrade guide

* wip

* Fix code styling

* wip

* wip

* wip

* wip

* wip

---------

Co-authored-by: driesvints <[email protected]>
  • Loading branch information
driesvints and driesvints authored Nov 19, 2023
1 parent e93aece commit 7ddef90
Show file tree
Hide file tree
Showing 15 changed files with 677 additions and 5 deletions.
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

0 comments on commit 7ddef90

Please sign in to comment.