Files
shopPRO/autoload/Domain/Product/ProductRepository.php

401 lines
13 KiB
PHP

<?php
namespace Domain\Product;
/**
* Repository odpowiedzialny za dostęp do danych produktów
*
* Zgodnie z wzorcem Repository Pattern, ta klasa enkapsuluje
* logikę dostępu do bazy danych dla produktów.
*/
class ProductRepository
{
private const MAX_PER_PAGE = 100;
/**
* @var \medoo Instancja Medoo ORM
*/
private $db;
/**
* Konstruktor - przyjmuje instancję bazy danych
*
* @param \medoo $db Instancja Medoo ORM
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Pobiera stan magazynowy produktu
*
* @param int $productId ID produktu
* @return int|null Ilość produktu lub null jeśli nie znaleziono
*/
public function getQuantity(int $productId): ?int
{
$quantity = $this->db->get('pp_shop_products', 'quantity', ['id' => $productId]);
// Medoo zwraca false jeśli nie znaleziono
return $quantity !== false ? (int)$quantity : null;
}
/**
* Pobiera produkt po ID
*
* @param int $productId ID produktu
* @return array|null Dane produktu lub null
*/
public function find(int $productId): ?array
{
$product = $this->db->get('pp_shop_products', '*', ['id' => $productId]);
return $product ?: null;
}
/**
* Zwraca liste produktow z archiwum do panelu admin.
*
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listArchivedForAdmin(
array $filters,
string $sortColumn = 'id',
string $sortDir = 'DESC',
int $page = 1,
int $perPage = 10
): array {
$allowedSortColumns = [
'id' => 'psp.id',
'name' => 'name',
'price_brutto' => 'psp.price_brutto',
'price_brutto_promo' => 'psp.price_brutto_promo',
'quantity' => 'psp.quantity',
'combinations' => 'combinations',
];
$sortSql = $allowedSortColumns[$sortColumn] ?? 'psp.id';
$sortDir = strtoupper(trim($sortDir)) === 'ASC' ? 'ASC' : 'DESC';
$page = max(1, $page);
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
$offset = ($page - 1) * $perPage;
$where = ['psp.archive = 1', 'psp.parent_id IS NULL'];
$params = [];
$phrase = trim((string)($filters['phrase'] ?? ''));
if (strlen($phrase) > 255) {
$phrase = substr($phrase, 0, 255);
}
if ($phrase !== '') {
$where[] = '(
psp.ean LIKE :phrase
OR psp.sku LIKE :phrase
OR EXISTS (
SELECT 1
FROM pp_shop_products_langs AS pspl2
WHERE pspl2.product_id = psp.id
AND pspl2.name LIKE :phrase
)
)';
$params[':phrase'] = '%' . $phrase . '%';
}
$whereSql = implode(' AND ', $where);
$sqlCount = "
SELECT COUNT(0)
FROM pp_shop_products AS psp
WHERE {$whereSql}
";
$stmtCount = $this->db->query($sqlCount, $params);
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
$sql = "
SELECT
psp.id,
psp.price_brutto,
psp.price_brutto_promo,
psp.quantity,
psp.sku,
psp.ean,
(
SELECT pspl.name
FROM pp_shop_products_langs AS pspl
INNER JOIN pp_langs AS pl ON pl.id = pspl.lang_id
WHERE pspl.product_id = psp.id
AND pspl.name <> ''
ORDER BY pl.o ASC
LIMIT 1
) AS name,
(
SELECT pspi.src
FROM pp_shop_products_images AS pspi
WHERE pspi.product_id = psp.id
ORDER BY pspi.o ASC, pspi.id ASC
LIMIT 1
) AS image_src,
(
SELECT pspi.alt
FROM pp_shop_products_images AS pspi
WHERE pspi.product_id = psp.id
ORDER BY pspi.o ASC, pspi.id ASC
LIMIT 1
) AS image_alt,
(
SELECT COUNT(0)
FROM pp_shop_products AS pspc
WHERE pspc.parent_id = psp.id
) AS combinations
FROM pp_shop_products AS psp
WHERE {$whereSql}
ORDER BY {$sortSql} {$sortDir}, psp.id {$sortDir}
LIMIT {$perPage} OFFSET {$offset}
";
$stmt = $this->db->query($sql, $params);
$items = $stmt ? $stmt->fetchAll() : [];
return [
'items' => is_array($items) ? $items : [],
'total' => $total,
];
}
/**
* Pobiera cenę produktu (promocyjną jeśli jest niższa, w przeciwnym razie regularną)
*
* @param int $productId ID produktu
* @return float|null Cena brutto lub null jeśli nie znaleziono
*/
public function getPrice(int $productId): ?float
{
$prices = $this->db->get('pp_shop_products', ['price_brutto', 'price_brutto_promo'], ['id' => $productId]);
if (!$prices) {
return null;
}
if ($prices['price_brutto_promo'] != '' && $prices['price_brutto_promo'] < $prices['price_brutto']) {
return (float)$prices['price_brutto_promo'];
}
return (float)$prices['price_brutto'];
}
/**
* Pobiera nazwę produktu w danym języku
*
* @param int $productId ID produktu
* @param string $langId ID języka
* @return string|null Nazwa produktu lub null jeśli nie znaleziono
*/
public function getName(int $productId, string $langId): ?string
{
$name = $this->db->get('pp_shop_products_langs', 'name', ['AND' => ['product_id' => $productId, 'lang_id' => $langId]]);
return $name ?: null;
}
/**
* Aktualizuje ilość produktu
*
* @param int $productId ID produktu
* @param int $quantity Nowa ilość
* @return bool Czy aktualizacja się powiodła
*/
public function updateQuantity(int $productId, int $quantity): bool
{
$result = $this->db->update(
'pp_shop_products',
['quantity' => $quantity],
['id' => $productId]
);
return $result !== false;
}
/**
* Przywraca produkt z archiwum (wraz z kombinacjami)
*
* @param int $productId ID produktu
* @return bool Czy operacja się powiodła
*/
public function unarchive(int $productId): bool
{
$this->db->update( 'pp_shop_products', [ 'status' => 1, 'archive' => 0 ], [ 'id' => $productId ] );
$this->db->update( 'pp_shop_products', [ 'status' => 1, 'archive' => 0 ], [ 'parent_id' => $productId ] );
return true;
}
/**
* Przenosi produkt do archiwum (wraz z kombinacjami)
*
* @param int $productId ID produktu
* @return bool Czy operacja się powiodła
*/
public function archive(int $productId): bool
{
$this->db->update( 'pp_shop_products', [ 'status' => 0, 'archive' => 1 ], [ 'id' => $productId ] );
$this->db->update( 'pp_shop_products', [ 'status' => 0, 'archive' => 1 ], [ 'parent_id' => $productId ] );
return true;
}
/**
* Pobiera listę wszystkich produktów głównych (id => name) do masowej edycji.
* Zwraca tylko produkty bez parent_id (bez kombinacji).
*
* @return array<int, string> Mapa id => nazwa produktu
*/
public function allProductsForMassEdit(): array
{
$defaultLang = $this->db->get( 'pp_langs', 'id', [ 'start' => 1 ] );
if ( !$defaultLang ) {
$defaultLang = 'pl';
}
$results = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => null ] );
$products = [];
if ( is_array( $results ) ) {
foreach ( $results as $id ) {
$name = $this->db->get( 'pp_shop_products_langs', 'name', [
'AND' => [ 'product_id' => $id, 'lang_id' => $defaultLang ]
] );
$products[ (int) $id ] = $name ?: '';
}
}
return $products;
}
/**
* Pobiera listę ID produktów przypisanych do danej kategorii.
*
* @param int $categoryId ID kategorii
* @return int[] Lista ID produktów
*/
public function getProductsByCategory(int $categoryId): array
{
$results = $this->db->select(
'pp_shop_products_categories',
'product_id',
[ 'category_id' => $categoryId ]
);
return is_array( $results ) ? $results : [];
}
/**
* Aplikuje rabat procentowy na produkt (cena promocyjna = cena - X%).
* Aktualizuje również ceny kombinacji produktu.
*
* @param int $productId ID produktu
* @param float $discountPercent Procent rabatu
* @return array|null Tablica z price_brutto i price_brutto_promo lub null przy błędzie
*/
public function applyDiscountPercent(int $productId, float $discountPercent): ?array
{
$product = $this->db->get( 'pp_shop_products', [
'vat', 'price_brutto', 'price_netto'
], [ 'id' => $productId ] );
if ( !$product ) {
return null;
}
$vat = $product['vat'];
$priceBrutto = (float) $product['price_brutto'];
$priceNetto = (float) $product['price_netto'];
$priceBruttoPromo = $priceBrutto - ( $priceBrutto * ( $discountPercent / 100 ) );
$priceNettoPromo = $priceNetto - ( $priceNetto * ( $discountPercent / 100 ) );
if ( $priceBrutto == $priceBruttoPromo ) {
$priceBruttoPromo = null;
}
if ( $priceNetto == $priceNettoPromo ) {
$priceNettoPromo = null;
}
$this->db->update( 'pp_shop_products', [
'price_brutto_promo' => $priceBruttoPromo,
'price_netto_promo' => $priceNettoPromo
], [ 'id' => $productId ] );
$this->updateCombinationPrices( $productId, $priceNetto, $vat, $priceNettoPromo );
return [
'price_brutto' => $priceBrutto,
'price_brutto_promo' => $priceBruttoPromo
];
}
/**
* Aktualizuje ceny kombinacji produktu uwzględniając wpływ na cenę (impact_on_the_price).
*
* @param int $productId ID produktu nadrzędnego
* @param float $priceNetto Cena netto bazowa
* @param float $vat Stawka VAT
* @param float|null $priceNettoPromo Cena promo netto bazowa (null = brak)
*/
private function updateCombinationPrices(int $productId, float $priceNetto, float $vat, ?float $priceNettoPromo): void
{
$priceBrutto = \S::normalize_decimal( $priceNetto * ( 100 + $vat ) / 100, 2 );
$priceBruttoPromo = $priceNettoPromo !== null
? \S::normalize_decimal( $priceNettoPromo * ( 100 + $vat ) / 100, 2 )
: null;
$combinations = $this->db->query(
'SELECT psp.id '
. 'FROM pp_shop_products AS psp '
. 'INNER JOIN pp_shop_products_attributes AS pspa ON psp.id = pspa.product_id '
. 'INNER JOIN pp_shop_attributes_values AS psav ON pspa.value_id = psav.id '
. 'WHERE psav.impact_on_the_price > 0 AND psp.parent_id = :product_id',
[ ':product_id' => $productId ]
);
if ( !$combinations ) {
return;
}
$rows = $combinations->fetchAll( \PDO::FETCH_ASSOC );
foreach ( $rows as $row ) {
$combBrutto = $priceBrutto;
$combBruttoPromo = $priceBruttoPromo;
$values = $this->db->query(
'SELECT impact_on_the_price FROM pp_shop_attributes_values AS psav '
. 'INNER JOIN pp_shop_products_attributes AS pspa ON pspa.value_id = psav.id '
. 'WHERE impact_on_the_price IS NOT NULL AND product_id = :product_id',
[ ':product_id' => $row['id'] ]
);
if ( $values ) {
foreach ( $values->fetchAll( \PDO::FETCH_ASSOC ) as $value ) {
$combBrutto += $value['impact_on_the_price'];
if ( $combBruttoPromo !== null ) {
$combBruttoPromo += $value['impact_on_the_price'];
}
}
}
$combNetto = \S::normalize_decimal( $combBrutto / ( 100 + $vat ) * 100, 2 );
$combNettoPromo = $combBruttoPromo !== null
? \S::normalize_decimal( $combBruttoPromo / ( 100 + $vat ) * 100, 2 )
: null;
$this->db->update( 'pp_shop_products', [
'price_netto' => $combNetto,
'price_brutto' => $combBrutto,
'price_netto_promo' => $combNettoPromo,
'price_brutto_promo' => $combBruttoPromo
], [ 'id' => $row['id'] ] );
}
}
}