Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1f412fe
docs(043): log open questions, create spec stub, update roadmap
Copilot May 31, 2026
c20d934
feat(043): resolve open questions, update spec and create plan/tasks …
Copilot May 31, 2026
91136c9
docs: log 12 F-043 open questions into open-questions.md
Copilot May 31, 2026
577d162
feat(043): add print/pixel size support backend implementation + tests
Copilot May 31, 2026
c996b2d
refactor(Order): clarify canProcessPayment email/shipping fall-throug…
Copilot May 31, 2026
5d0b9f7
Merge branch 'master' into copilot/feature-43-add-print-and-pixel-siz…
ildyria Jun 1, 2026
77622f1
Merge branch 'master' into copilot/feature-43-add-print-and-pixel-siz…
ildyria Jun 3, 2026
1d7b582
Merge branch 'master' into copilot/feature-43-add-print-and-pixel-siz…
ildyria Jun 3, 2026
817687d
fix phpstan + some tests
ildyria Jun 3, 2026
8990528
fix more tests
ildyria Jun 4, 2026
605ecc4
fix more tests
ildyria Jun 4, 2026
5172d58
Fix TS duplicate content and add missing lychee.d.ts types for print/…
Copilot Jun 4, 2026
4e1ad0a
fix test
ildyria Jun 4, 2026
02422c3
Fixes
ildyria Jun 5, 2026
2f6bfc2
fix tests
ildyria Jun 7, 2026
a9bb466
Fixes
ildyria Jun 8, 2026
3d6d348
fix purchasable creation
ildyria Jun 9, 2026
847dbe9
Improve feedback + fix bugs
ildyria Jun 10, 2026
d0b415d
Add license type to Pixel sizes
ildyria Jun 10, 2026
d2c284d
fix test
ildyria Jun 11, 2026
9dd899a
fix test
ildyria Jun 11, 2026
f665bd9
fix test
ildyria Jun 11, 2026
6dae7d1
do not promote directly the Orders of pixel & prints
ildyria Jun 11, 2026
a17024f
fixes
ildyria Jun 11, 2026
0192875
fixes
ildyria Jun 11, 2026
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 Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
# ============================================================================
# Stage 3: Production FrankenPHP Image
# ============================================================================
FROM dunglas/frankenphp:php8.5-trixie@sha256:b2b64b403c6dbfdcb6cdb78d533ff89d131eb9d5d8aba597e15a46559341f3b4
FROM dunglas/frankenphp:php8.5-trixie@sha256:932495e768c843729b043bfb0e40af6143b1bac98862d16aa93ea10d7338f8ed

ARG USER=appuser

Expand Down Expand Up @@ -120,7 +120,7 @@
COPY --from=node --chown=www-data:www-data /app/public/embed ./public/embed

# Ensure storage and bootstrap/cache are writable with minimal permissions
RUN mkdir -p storage/framework/cache \

Check failure on line 123 in Dockerfile

View workflow job for this annotation

GitHub Actions / 3️⃣ Dockerfile Lint

SC2086 info: Double quote to prevent globbing and word splitting.
storage/framework/sessions \
storage/framework/views \
storage/logs \
Expand Down
66 changes: 65 additions & 1 deletion app/Actions/Shop/BasketService.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use App\Models\Album;
use App\Models\Order;
use App\Models\Photo;
use App\Models\PixelSize;
use App\Models\PrintSize;
use App\Models\User;
use App\Policies\AlbumPolicy;
use App\Policies\AlbumQueryPolicy;
Expand Down Expand Up @@ -184,7 +186,69 @@ public function addAlbumToBasket(
}

/**
* Remove an item from the basket.
* Add a photo to the basket as a physical print item.
*
* @param Order $basket The basket to add to
* @param Photo $photo The photo to add
* @param string $album_id The album ID the photo belongs to
* @param PrintSize $print_size The print size
* @param string|null $notes Optional notes for the item
*
* @return Order The updated basket
*/
public function addPrintItemToBasket(
Order $basket,
Photo $photo,
string $album_id,
PrintSize $print_size,
?string $notes = null,
): Order {
$this->ensurePendingStatus($basket);
$basket = $this->order_service->addPrintPhotoToOrder(
$basket,
$photo,
$album_id,
$print_size,
$notes
);

return $this->order_service->refreshBasket($basket);
}

/**
* Add a photo to the basket as a custom pixel-size digital download.
*
* @param Order $basket The basket to add to
* @param Photo $photo The photo to add
* @param string $album_id The album ID the photo belongs to
* @param PixelSize $pixel_size The pixel size
* @param string|null $notes Optional notes for the item
*
* @return Order The updated basket
*/
public function addPixelItemToBasket(
Order $basket,
Photo $photo,
string $album_id,
PixelSize $pixel_size,
PurchasableLicenseType $license_type,
?string $notes = null,
): Order {
$this->ensurePendingStatus($basket);
$basket = $this->order_service->addPixelPhotoToOrder(
$basket,
$photo,
$album_id,
$pixel_size,
$license_type,
$notes
);

return $this->order_service->refreshBasket($basket);
}

/**
* Remove an item from the basket. *.
*
* @param Order $basket The basket to remove from
* @param int $item_id The ID of the order item to remove
Expand Down
117 changes: 117 additions & 0 deletions app/Actions/Shop/OrderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Photo;
use App\Models\PixelSize;
use App\Models\PrintSize;
use App\Models\User;
use App\Repositories\ConfigManager;
use Illuminate\Database\Eloquent\Builder;
Expand Down Expand Up @@ -176,6 +178,121 @@ public function addPhotoToOrder(
return $order;
}

/**
* Add a photo to an order as a physical print item.
*
* @param Order $order The order to add to
* @param Photo $photo The photo to add
* @param string $album_id The album ID to consider for hierarchical pricing
* @param PrintSize $print_size The print size to use
* @param string|null $notes Additional notes for the item
*
* @return Order The updated Order (note: total not recalculated)
*
* @throws PhotoNotPurchasableException If the photo is not available for purchase
* @throws InvalidPurchaseOptionException If the print size is not available for this purchasable
*/
public function addPrintPhotoToOrder(
Order $order,
Photo $photo,
string $album_id,
PrintSize $print_size,
?string $notes = null,
): Order {
$purchasable = $this->purchasable_service->getEffectivePurchasableForPhoto($photo, $album_id);

if ($purchasable === null) {
throw new PhotoNotPurchasableException();
}

/** @var \App\Models\PurchasablePrintSize|null $assignment */
$assignment = $purchasable->printSizes()
->where('print_size_id', $print_size->id)
->first();

if ($assignment === null) {
throw new InvalidPurchaseOptionException();
}

$assignment->load('printSize');

OrderItem::create([
'order_id' => $order->id,
'purchasable_id' => $purchasable->id,
'photo_id' => $photo->id,
'album_id' => $album_id,
'title' => $photo->title ?? "Photo #{$photo->id}",
'license_type' => PurchasableLicenseType::PRINT,
'price_cents' => $assignment->price_cents,
'size_variant_type' => PurchasableSizeVariantType::ORIGINAL,
'is_print' => true,
'print_size_id' => $print_size->id,
'print_width' => $print_size->width,
'print_height' => $print_size->height,
'print_unit' => $print_size->unit,
'print_paper_type' => $print_size->paper_type,
'item_notes' => $notes,
]);

return $order;
}

/**
* Add a photo to an order as a custom pixel-size digital download.
*
* @param Order $order The order to add to
* @param Photo $photo The photo to add
* @param string $album_id The album ID to consider for hierarchical pricing
* @param PixelSize $pixel_size The pixel size to use
* @param string|null $notes Additional notes for the item
*
* @return Order The updated Order (note: total not recalculated)
*
* @throws PhotoNotPurchasableException If the photo is not available for purchase
* @throws InvalidPurchaseOptionException If the pixel size is not available for this purchasable
*/
public function addPixelPhotoToOrder(
Order $order,
Photo $photo,
string $album_id,
PixelSize $pixel_size,
PurchasableLicenseType $license_type,
?string $notes = null,
): Order {
$purchasable = $this->purchasable_service->getEffectivePurchasableForPhoto($photo, $album_id);

if ($purchasable === null) {
throw new PhotoNotPurchasableException();
}

/** @var \App\Models\PurchasablePixelSize|null $assignment */
$assignment = $purchasable->pixelSizes()
->where('pixel_size_id', $pixel_size->id)
->where('license_type', $license_type->value)
->first();

if ($assignment === null) {
throw new InvalidPurchaseOptionException();
}

OrderItem::create([
'order_id' => $order->id,
'purchasable_id' => $purchasable->id,
'photo_id' => $photo->id,
'album_id' => $album_id,
'title' => $photo->title ?? "Photo #{$photo->id}",
'license_type' => $assignment->license_type,
'price_cents' => $assignment->price_cents,
'size_variant_type' => PurchasableSizeVariantType::ORIGINAL,
'pixel_size_id' => $pixel_size->id,
'pixel_width' => $pixel_size->width,
'pixel_height' => $pixel_size->height,
'item_notes' => $notes,
]);

return $order;
}

/**
* Refresh basket data and recalculate order total.
*
Expand Down
55 changes: 55 additions & 0 deletions app/Actions/Shop/PurchasableService.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
namespace App\Actions\Shop;

use App\Constants\PhotoAlbum as PA;
use App\DTO\PixelSizeAssignment;
use App\DTO\PrintSizeAssignment;
use App\DTO\PurchasableOption;
use App\DTO\PurchasableOptionCreate;
use App\Exceptions\Internal\LycheeLogicException;
use App\Models\Album;
use App\Models\Photo;
use App\Models\Purchasable;
use App\Models\PurchasablePixelSize;
use App\Models\PurchasablePrice;
use App\Models\PurchasablePrintSize;
use Illuminate\Support\Facades\DB;

class PurchasableService
Expand Down Expand Up @@ -296,4 +300,55 @@ public function deleteMultipleAlbumPurchasables(array $album_ids): void
->whereIn('album_id', $album_ids)
->delete();
}

/**
* Sync print size assignments for a purchasable item.
*
* Replaces all existing print size assignments with the provided list.
*
* @param Purchasable $purchasable The purchasable item to update
* @param PrintSizeAssignment[] $print_size_assignments Array of print size assignments
*
* @return Purchasable The updated purchasable item
*/
public function syncPrintSizes(Purchasable $purchasable, array $print_size_assignments): Purchasable
{
$purchasable->printSizes()->delete();

foreach ($print_size_assignments as $assignment) {
PurchasablePrintSize::create([
'purchasable_id' => $purchasable->id,
'print_size_id' => $assignment->print_size_id,
'price_cents' => $assignment->price,
]);
}

return $purchasable;
}

/**
* Sync pixel size assignments for a purchasable item.
*
* Replaces all existing pixel size assignments with the provided list.
*
* @param Purchasable $purchasable The purchasable item to update
* @param PixelSizeAssignment[] $pixel_size_assignments Array of pixel size assignments
*
* @return Purchasable The updated purchasable item
*/
public function syncPixelSizes(Purchasable $purchasable, array $pixel_size_assignments): Purchasable
{
$purchasable->pixelSizes()->delete();

foreach ($pixel_size_assignments as $assignment) {
PurchasablePixelSize::create([
'purchasable_id' => $purchasable->id,
'pixel_size_id' => $assignment->pixel_size_id,
'price_cents' => $assignment->price,
'license_type' => $assignment->license_type,
]);
}

return $purchasable;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
2 changes: 2 additions & 0 deletions app/Contracts/Http/Requests/RequestAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ class RequestAttribute
public const BASKET_ID_ATTRIBUTE = 'basket_id';
public const TRANSACTION_ID_ATTRIBUTE = 'transaction_id';
public const PRICES_ATTRIBUTE = 'prices';
public const PRINT_SIZES_ATTRIBUTE = 'print_sizes';
public const PIXEL_SIZES_ATTRIBUTE = 'pixel_sizes';
public const SIZE_VARIANT_TYPE_ATTRIBUTE = 'size_variant_type';
public const LICENSE_TYPE_ATTRIBUTE = 'license_type';
public const IS_ACTIVE_ATTRIBUTE = 'is_active';
Expand Down
22 changes: 22 additions & 0 deletions app/DTO/PixelSizeAssignment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\DTO;

use App\Enum\PurchasableLicenseType;
use Money\Money;

readonly class PixelSizeAssignment
{
public function __construct(
public int $pixel_size_id,
public Money $price,
public PurchasableLicenseType $license_type = PurchasableLicenseType::PERSONAL,
) {
}
}
20 changes: 20 additions & 0 deletions app/DTO/PrintSizeAssignment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\DTO;

use Money\Money;

readonly class PrintSizeAssignment
{
public function __construct(
public int $print_size_id,
public Money $price,
) {
}
}
1 change: 1 addition & 0 deletions app/Enum/PurchasableLicenseType.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ enum PurchasableLicenseType: string
case PERSONAL = 'personal';
case COMMERCIAL = 'commercial';
case EXTENDED = 'extended';
case PRINT = 'print';
}
Loading
Loading