Skip to content

Commit b8cf953

Browse files
committed
Copy digital products code into core
1 parent f267354 commit b8cf953

16 files changed

+480
-3
lines changed

docs/addons/digital-products.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ As of v3.1.0, the Digital Products addon supports adding different 'downloadable
3838

3939
By default, we create a serial license key which you can give to your customers. However, you may want to customise where the code comes from or maybe you want to send it away to a third party service.
4040

41-
To do this, you can create your own license key repository which [implements the one provided by this addon](https://github.com/doublethreedigital/sc-digital-products/blob/master/src/Contracts/LicenseKeyRepository.php).
41+
To do this, you can create your own license key repository which [implements the one provided by this addon](https://github.com/doublethreedigital/simple-commerce/blob/main/src/Contracts/LicenseKeyRepository.php).
4242

4343
To register your repository, you'll need to bind it to our `LicenseKey` facade. You can do this in your `AppServiceProvider`.
4444

@@ -53,7 +53,7 @@ If you'd like the Digital Products addon to send your customers an email notific
5353
```php
5454
'notifications' => [
5555
'digital_download_ready' => [
56-
\DoubleThreeDigital\DigitalProducts\Notifications\DigitalDownloadsNotification::class => [
56+
\DoubleThreeDigital\SimpleCommerce\Notifications\DigitalDownloadsNotification::class => [
5757
'to' => 'customer',
5858
],
5959
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@component('mail::message')
2+
# Digital Downloads Ready
3+
4+
Your order, **#{{ $order->orderNumber() }}** has some downloadable items. We've provided links to each of the items below.
5+
6+
## Downloads
7+
8+
@component('mail::table')
9+
| Items | Download |
10+
| :--------- | :------------- |
11+
@foreach ($order->lineItems() as $lineItem)
12+
| [{{ $lineItem->product()->get('title') }}]({{ optional($lineItem->product()->resource())->absoluteUrl() }}) | [Download]({{ $lineItem->metadata->get('download_url') }}) |
13+
@endforeach
14+
@endcomponent
15+
16+
Thanks,<br>
17+
{{ config('app.name') }}
18+
@endcomponent

routes/actions.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
use DoubleThreeDigital\SimpleCommerce\Http\Controllers\GatewayCallbackController;
99
use DoubleThreeDigital\SimpleCommerce\Http\Controllers\GatewayWebhookController;
1010
use DoubleThreeDigital\SimpleCommerce\Http\Middleware\EnsureFormParametersArriveIntact;
11+
use DoubleThreeDigital\SimpleCommerce\Http\Controllers\DigitalProducts\DownloadController;
12+
use DoubleThreeDigital\SimpleCommerce\Http\Controllers\DigitalProducts\VerificationController;
1113
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
1214
use Illuminate\Support\Facades\Route;
1315

14-
Route::namespace('\DoubleThreeDigital\SimpleCommerce\Http\Controllers\Actions')->name('simple-commerce.')->group(function () {
16+
Route::name('simple-commerce.')->group(function () {
1517
Route::get('/cart', [CartController::class, 'index'])->name('cart.index');
1618
Route::get('/customer/{customer}', [CustomerController::class, 'index'])->name('customer.index');
1719

@@ -36,4 +38,9 @@
3638
Route::post('/gateways/{gateway}/webhook', [GatewayWebhookController::class, 'index'])
3739
->name('gateways.webhook')
3840
->withoutMiddleware([VerifyCsrfToken::class]);
41+
42+
Route::prefix('digital-products')->name('digital-products.')->group(function () {
43+
Route::get('download/{orderId}/{lineItemId}', DownloadController::class)->name('download');
44+
Route::post('verification', VerificationController::class)->name('verification');
45+
});
3946
});
File renamed without changes.
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace DoubleThreeDigital\SimpleCommerce\Contracts;
4+
5+
/**
6+
* @mixin \DoubleThreeDigital\SimpleCommerce\Products\DigitalProducts\LicenseKeyRepository
7+
*/
8+
interface LicenseKeyRepository
9+
{
10+
public function generate(): string;
11+
12+
public static function bindings(): array;
13+
}

src/Events/DigitalDownloadReady.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace DoubleThreeDigital\SimpleCommerce\Events;
4+
5+
use DoubleThreeDigital\SimpleCommerce\Contracts\Order;
6+
7+
class DigitalDownloadReady
8+
{
9+
public function __construct(public Order $order)
10+
{
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace DoubleThreeDigital\SimpleCommerce\Notifications;
4+
5+
use DoubleThreeDigital\SimpleCommerce\Contracts\Order;
6+
use Illuminate\Bus\Queueable;
7+
use Illuminate\Notifications\Messages\MailMessage;
8+
use Illuminate\Notifications\Notification;
9+
10+
class DigitalDownloadsNotification extends Notification
11+
{
12+
use Queueable;
13+
14+
protected $order;
15+
16+
/**
17+
* Create a new notification instance.
18+
*
19+
* @return void
20+
*/
21+
public function __construct(Order $order)
22+
{
23+
$this->order = $order;
24+
}
25+
26+
/**
27+
* Get the notification's delivery channels.
28+
*
29+
* @param mixed $notifiable
30+
* @return array
31+
*/
32+
public function via($notifiable)
33+
{
34+
return [
35+
'mail',
36+
];
37+
}
38+
39+
/**
40+
* Get the mail representation of the notification.
41+
*
42+
* @param mixed $notifiable
43+
* @return \Illuminate\Notifications\Messages\MailMessage
44+
*/
45+
public function toMail($notifiable)
46+
{
47+
return (new MailMessage)
48+
->subject(__(':siteName: Downloads Ready', ['siteName' => config('app.name')]))
49+
->markdown('simple-commerce::emails.customer_download', [
50+
'order' => $this->order,
51+
]);
52+
}
53+
}

src/Facades/LicenseKey.php

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace DoubleThreeDigital\SimpleCommerce\Facades;
4+
5+
use DoubleThreeDigital\SimpleCommerce\Contracts\LicenseKeyRepository;
6+
use Illuminate\Support\Facades\Facade;
7+
8+
/**
9+
* @see \DoubleThreeDigital\SimpleCommerce\Products\DigitalProducts\LicenseKeyRepository
10+
*/
11+
class LicenseKey extends Facade
12+
{
13+
protected static function getFacadeAccessor()
14+
{
15+
return LicenseKeyRepository::class;
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
namespace DoubleThreeDigital\SimpleCommerce\Http\Controllers\DigitalProducts;
4+
5+
use DoubleThreeDigital\SimpleCommerce\Facades\Order;
6+
use DoubleThreeDigital\SimpleCommerce\Products\ProductType;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Routing\Controller;
9+
use Statamic\Assets\Asset;
10+
use Statamic\Facades\AssetContainer;
11+
use ZipArchive;
12+
13+
class DownloadController extends Controller
14+
{
15+
public function __invoke(Request $request)
16+
{
17+
$order = Order::find($request->order_id);
18+
$item = $order->lineItems()->firstWhere('id', $request->item_id);
19+
20+
if (! $item->metadata()->has('license_key') || $item->metadata()->get('license_key') !== $request->get('license_key')) {
21+
abort(401);
22+
}
23+
24+
$product = $item->product();
25+
26+
$zip = new ZipArchive;
27+
$zip->open(storage_path("{$order->id()}__{$item->id()}__{$product->id()}.zip"), ZipArchive::CREATE | ZipArchive::OVERWRITE);
28+
29+
if ($product->purchasableType() === ProductType::Product) {
30+
if (! $product->has('downloadable_asset')) {
31+
throw new \Exception("Product [{$product->id()}] does not have any digital downloadable assets.");
32+
}
33+
34+
$product->toAugmentedArray()['downloadable_asset']->value()->get()
35+
->each(function (Asset $asset) use ($request, $order, $item, $product, &$zip) {
36+
if ($item->metadata()->has('download_history') && $product->has('download_limit')) {
37+
if (collect($item->metadata()->get('download_history'))->count() >= $product->get('download_limit')) {
38+
abort(405, "You've reached the download limit for this product.");
39+
}
40+
}
41+
42+
$order->updateLineItem($item->id(), [
43+
'metadata' => array_merge($item->metadata()->toArray(), [
44+
'download_history' => array_merge([
45+
[
46+
'timestamp' => now()->timestamp,
47+
'ip_address' => $request->ip(),
48+
],
49+
], $item->metadata()->get('download_history', [])),
50+
]),
51+
]);
52+
53+
$zip->addFile($asset->resolvedPath(), "{$product->get('slug')}/{$asset->basename()}");
54+
});
55+
}
56+
57+
if ($product->purchasableType() === ProductType::Variant) {
58+
$productVariant = $product->variant($item->variant()['variant']);
59+
60+
if (! $productVariant->has('downloadable_asset')) {
61+
throw new \Exception("Product [{$product->id()}] does not have any digital downloadable assets.");
62+
}
63+
64+
$productVariantsField = $product->resource()->blueprint()->field('product_variants');
65+
66+
$downloadableAssetField = collect($productVariantsField->get('option_fields'))
67+
->where('handle', 'downloadable_asset')
68+
->first();
69+
70+
collect($productVariant->get('downloadable_asset'))
71+
->map(function ($assetPath) use ($downloadableAssetField) {
72+
$assetContainer = isset($downloadableAssetField['field']['container'])
73+
? AssetContainer::findByHandle($downloadableAssetField['field']['container'])
74+
: AssetContainer::all()->first();
75+
76+
return $assetContainer->asset($assetPath);
77+
})
78+
->each(function (Asset $asset) use ($request, $order, $item, $product, $productVariant, &$zip) {
79+
if (config('sc-digital-products.download_history')) {
80+
if ($item->metadata()->has('download_history') && $productVariant->has('download_limit') && $product->get('download_limit') !== null) {
81+
if (collect($item->metadata()->get('download_history'))->count() >= $productVariant->get('download_limit')) {
82+
abort(405, "You've reached the download limit for this product.");
83+
}
84+
}
85+
86+
$order->updateLineItem($item->id(), [
87+
'metadata' => array_merge($item->metadata()->toArray(), [
88+
'download_history' => array_merge([
89+
[
90+
'timestamp' => now()->timestamp,
91+
'ip_address' => $request->ip(),
92+
],
93+
], $item->metadata()->get('download_history', [])),
94+
]),
95+
]);
96+
}
97+
98+
$zip->addFile($asset->resolvedPath(), "{$productVariant->key()}/{$asset->basename()}");
99+
});
100+
}
101+
102+
$zip->close();
103+
104+
return response()->download(storage_path("{$order->id()}__{$item->id()}__{$product->id()}.zip"), "{$product->get('slug')}.zip");
105+
}
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace DoubleThreeDigital\SimpleCommerce\Http\Controllers\DigitalProducts;
4+
5+
use DoubleThreeDigital\SimpleCommerce\Orders\EloquentOrderRepository;
6+
use DoubleThreeDigital\SimpleCommerce\Orders\EntryOrderRepository;
7+
use DoubleThreeDigital\SimpleCommerce\Orders\OrderStatus;
8+
use DoubleThreeDigital\SimpleCommerce\SimpleCommerce;
9+
use Illuminate\Http\Request;
10+
use Illuminate\Routing\Controller;
11+
use Statamic\Facades\Entry;
12+
13+
class VerificationController extends Controller
14+
{
15+
public function __invoke(Request $request)
16+
{
17+
// TODO: refactor controller to use $validated['license_key']
18+
$validated = $request->validate([
19+
'license_key' => ['required', 'string'],
20+
]);
21+
22+
// TODO: refactor query
23+
if ($this->isOrExtendsClass(SimpleCommerce::orderDriver()['repository'], EntryOrderRepository::class)) {
24+
$orderQuery = Entry::query()
25+
->where('collection', SimpleCommerce::orderDriver()['collection'])
26+
->whereIn('order_status', [
27+
OrderStatus::Placed->value,
28+
OrderStatus::Dispatched->value,
29+
])
30+
->where('items->0->metadata->license_key', $request->license_key)
31+
->orWhere('items->1->metadata->license_key', $request->license_key)
32+
->orWhere('items->2->metadata->license_key', $request->license_key)
33+
->orWhere('items->3->metadata->license_key', $request->license_key)
34+
->orWhere('items->4->metadata->license_key', $request->license_key)
35+
->orWhere('items->5->metadata->license_key', $request->license_key)
36+
->orWhere('items->6->metadata->license_key', $request->license_key)
37+
->orWhere('items->7->metadata->license_key', $request->license_key)
38+
->orWhere('items->8->metadata->license_key', $request->license_key)
39+
->orWhere('items->9->metadata->license_key', $request->license_key)
40+
->limit(1)
41+
->get();
42+
}
43+
44+
// TODO: refactor query
45+
if ($this->isOrExtendsClass(SimpleCommerce::orderDriver()['repository'], EloquentOrderRepository::class)) {
46+
$orderModel = new (SimpleCommerce::orderDriver()['model']);
47+
48+
$orderQuery = $orderModel::query()
49+
->whereIn('order_status', [
50+
OrderStatus::Placed->value,
51+
OrderStatus::Dispatched->value,
52+
])
53+
->whereRaw("JSON_EXTRACT(items, '$[0].metadata.license_key') = ?", [$request->license_key])
54+
->limit(1)
55+
->get();
56+
}
57+
58+
return $orderQuery->count() > 0
59+
? $this->validResponse($request)
60+
: $this->invalidResponse($request);
61+
}
62+
63+
protected function validResponse($request): array
64+
{
65+
return [
66+
'license_key' => $request->license_key,
67+
'valid' => true,
68+
];
69+
}
70+
71+
protected function invalidResponse($request): array
72+
{
73+
return [
74+
'license_key' => $request->license_key,
75+
'valid' => false,
76+
];
77+
}
78+
79+
protected function isOrExtendsClass(string $class, string $classToCheckAgainst): bool
80+
{
81+
return is_subclass_of($class, $classToCheckAgainst)
82+
|| $class === $classToCheckAgainst;
83+
}
84+
}

0 commit comments

Comments
 (0)