v1.0.0.0
StocketBase
- <?php
-
- declare(strict_types=1);
-
- if (count(get_included_files()) === 1) {
- http_response_code(403);
- exit;
- }
-
- require_once __DIR__ . '/../includes/slugger.php';
- require_once __DIR__ . '/../includes/currency.php';
-
- $title = trim((string) ($_POST['title'] ?? ''));
- $categoryId = (int) ($_POST['category_id'] ?? 0) ?: null;
- $excerpt = trim((string) ($_POST['excerpt'] ?? ''));
- $seoTitle = trim((string) ($_POST['seo_title'] ?? ''));
- $seoDescription = trim((string) ($_POST['seo_description'] ?? ''));
- $coverImage = trim((string) ($_POST['cover_image_path'] ?? ''));
- $blocksJson = BlockEditor::sanitize((string) ($_POST['blocks_json'] ?? '{"blocks":[]}'));
- $productId = (int) ($_POST['product_id'] ?? 0) ?: null;
- $requestedAction = $_POST['publish_action'] ?? 'save_draft';
- $scheduledInput = trim((string) ($_POST['scheduled_at'] ?? ''));
- $scheduledTimestamp = $scheduledInput !== '' ? strtotime($scheduledInput) : false;
- $scheduledAt = $scheduledTimestamp !== false ? date('Y-m-d H:i:s', $scheduledTimestamp) : '';
-
- $sku = trim((string) ($_POST['sku'] ?? ''));
- $productKind = in_array($_POST['product_kind'] ?? '', ['physical', 'digital', 'saas_plan'], true) ? $_POST['product_kind'] : 'physical';
- $currencyCode = Currency::normalize((string) ($_POST['currency'] ?? SiteFront::settings()['store_currency'] ?? 'USD'));
- $priceMajor = (int) ($_POST['price_major'] ?? 0);
- $priceMinor = (int) ($_POST['price_minor'] ?? 0);
- $priceCents = Currency::toMinor($priceMajor, $priceMinor, $currencyCode);
- $compareMajorRaw = trim((string) ($_POST['compare_price_major'] ?? ''));
- $compareAtPriceCents = $compareMajorRaw !== ''
- ? Currency::toMinor((int) $compareMajorRaw, (int) ($_POST['compare_price_minor'] ?? 0), $currencyCode)
- : null;
- $billingInterval = in_array($_POST['billing_interval'] ?? '', ['month', 'year', 'one_time'], true) ? $_POST['billing_interval'] : null;
- $trackInventory = isset($_POST['track_inventory']) ? 1 : 0;
- $stockQuantity = $trackInventory ? (int) ($_POST['stock_quantity'] ?? 0) : null;
- $weightGrams = (int) ($_POST['weight_grams'] ?? 0) ?: null;
- $stripePriceId = trim((string) ($_POST['stripe_price_id'] ?? ''));
- $polarProductId = trim((string) ($_POST['polar_product_id'] ?? ''));
-
- if ($title === '') {
- return ['Product title is required.', 'error', null];
- }
-
- $slug = trim((string) ($_POST['slug'] ?? ''));
- if ($slug === '') {
- $slug = Slugger::make($title);
- }
- $slug = $slug . '-' . substr(bin2hex(random_bytes(3)), 0, 6);
-
- $db = Database::site();
-
- $syncVariants = static function (int $productId) use ($db): void {
- $names = $_POST['variant_name'] ?? [];
- if (!is_array($names)) {
- return;
- }
-
- $db->prepare('DELETE FROM product_variants WHERE product_id = :id')->execute(['id' => $productId]);
-
- $skus = $_POST['variant_sku'] ?? [];
- $prices = $_POST['variant_price_cents'] ?? [];
- $stocks = $_POST['variant_stock'] ?? [];
- $insert = $db->prepare(
- 'INSERT INTO product_variants (product_id, name, sku, price_cents, stock_quantity, sort_order, is_default)
- VALUES (:product_id, :name, :sku, :price, :stock, :sort, :is_default)'
- );
-
- $sort = 0;
- foreach ($names as $index => $name) {
- $name = trim((string) $name);
- if ($name === '') {
- continue;
- }
- $insert->execute([
- 'product_id' => $productId,
- 'name' => $name,
- 'sku' => trim((string) ($skus[$index] ?? '')) ?: null,
- 'price' => is_numeric($prices[$index] ?? null) ? (int) round(((float) $prices[$index]) * 100) : null,
- 'stock' => is_numeric($stocks[$index] ?? null) ? (int) $stocks[$index] : null,
- 'sort' => $sort,
- 'is_default' => $sort === 0 ? 1 : 0,
- ]);
- $sort++;
- }
- };
-
- if ($productId !== null) {
- $existingStatement = $db->prepare('SELECT * FROM products WHERE id = :id LIMIT 1');
- $existingStatement->execute(['id' => $productId]);
- $existing = $existingStatement->fetch();
-
- if (!$existing) {
- return ['Product not found.', 'error', null];
- }
-
- if ($existing['deleted_at'] !== null) {
- return ['This product is in the trash. Restore it first.', 'error', null];
- }
-
- $isOwner = (int) $existing['created_by'] === (int) $currentUser['id'];
- $canEditAny = Auth::hasRoleAtLeast(Auth::ROLE_STORE_OWNER);
- $canProofread = Auth::role() === Auth::ROLE_CATALOG_ASSISTANT;
-
- if (!$isOwner && !$canEditAny && !$canProofread) {
- return ['You do not have permission to edit this product.', 'error', null];
- }
-
- if ($canProofread && !$isOwner) {
- $update = $db->prepare(
- 'UPDATE products SET content_blocks = :content, updated_at = NOW() WHERE id = :id'
- );
- $update->execute(['content' => $blocksJson, 'id' => $productId]);
- return ['Proofreading changes saved.', 'success', $productId];
- }
-
- $status = $existing['status'];
-
- if ($requestedAction === 'submit_for_review') {
- $status = 'pending_review';
- } elseif ($requestedAction === 'publish_now') {
- $status = Auth::canPublishDirectly() || $canEditAny ? 'published' : 'pending_review';
- } elseif ($requestedAction === 'schedule' && $scheduledAt !== '') {
- $status = 'scheduled';
- } elseif ($requestedAction === 'save_draft') {
- $status = 'draft';
- }
-
- $publishedAt = $status === 'published' ? ($existing['published_at'] ?? date('Y-m-d H:i:s')) : $existing['published_at'];
-
- $update = $db->prepare(
- 'UPDATE products SET category_id = :category_id, title = :title, excerpt = :excerpt,
- content_blocks = :content, cover_image_path = :cover, sku = :sku, product_kind = :product_kind,
- price_cents = :price_cents, compare_at_price_cents = :compare_price, currency = :currency,
- billing_interval = :billing_interval, track_inventory = :track_inventory, stock_quantity = :stock_quantity,
- weight_grams = :weight_grams, stripe_price_id = :stripe_price_id, polar_product_id = :polar_product_id,
- status = :status, seo_title = :seo_title, seo_description = :seo_description,
- scheduled_at = :scheduled_at, published_at = :published_at, updated_at = NOW()
- WHERE id = :id'
- );
- $update->execute([
- 'category_id' => $categoryId,
- 'title' => $title,
- 'excerpt' => $excerpt,
- 'content' => $blocksJson,
- 'cover' => $coverImage,
- 'sku' => $sku !== '' ? $sku : null,
- 'product_kind' => $productKind,
- 'price_cents' => $priceCents,
- 'compare_price' => $compareAtPriceCents,
- 'currency' => $currencyCode,
- 'billing_interval' => $billingInterval,
- 'track_inventory' => $trackInventory,
- 'stock_quantity' => $stockQuantity,
- 'weight_grams' => $weightGrams,
- 'stripe_price_id' => $stripePriceId !== '' ? $stripePriceId : null,
- 'polar_product_id' => $polarProductId !== '' ? $polarProductId : null,
- 'status' => $status,
- 'seo_title' => $seoTitle,
- 'seo_description' => $seoDescription,
- 'scheduled_at' => $status === 'scheduled' ? $scheduledAt : null,
- 'published_at' => $publishedAt,
- 'id' => $productId,
- ]);
-
- $syncVariants($productId);
-
- return ['Product updated.', 'success', $productId];
- }
-
- $status = 'draft';
- if ($requestedAction === 'submit_for_review') {
- $status = 'pending_review';
- } elseif ($requestedAction === 'publish_now') {
- $status = Auth::canPublishDirectly() ? 'published' : 'pending_review';
- } elseif ($requestedAction === 'schedule' && $scheduledAt !== '') {
- $status = 'scheduled';
- }
-
- $insert = $db->prepare(
- 'INSERT INTO products (created_by, category_id, title, slug, excerpt, content_blocks, cover_image_path,
- sku, product_kind, price_cents, compare_at_price_cents, currency, billing_interval, track_inventory, stock_quantity,
- weight_grams, stripe_price_id, polar_product_id, status, seo_title, seo_description, scheduled_at, published_at)
- VALUES (:created_by, :category_id, :title, :slug, :excerpt, :content, :cover,
- :sku, :product_kind, :price_cents, :compare_price, :currency, :billing_interval, :track_inventory, :stock_quantity,
- :weight_grams, :stripe_price_id, :polar_product_id, :status, :seo_title, :seo_description, :scheduled_at, :published_at)'
- );
- $insert->execute([
- 'created_by' => $currentUser['id'],
- 'category_id' => $categoryId,
- 'title' => $title,
- 'slug' => $slug,
- 'excerpt' => $excerpt,
- 'content' => $blocksJson,
- 'cover' => $coverImage,
- 'sku' => $sku !== '' ? $sku : null,
- 'product_kind' => $productKind,
- 'price_cents' => $priceCents,
- 'compare_price' => $compareAtPriceCents,
- 'currency' => $currencyCode,
- 'billing_interval' => $billingInterval,
- 'track_inventory' => $trackInventory,
- 'stock_quantity' => $stockQuantity,
- 'weight_grams' => $weightGrams,
- 'stripe_price_id' => $stripePriceId !== '' ? $stripePriceId : null,
- 'polar_product_id' => $polarProductId !== '' ? $polarProductId : null,
- 'status' => $status,
- 'seo_title' => $seoTitle,
- 'seo_description' => $seoDescription,
- 'scheduled_at' => $status === 'scheduled' ? $scheduledAt : null,
- 'published_at' => $status === 'published' ? date('Y-m-d H:i:s') : null,
- ]);
-
- $newId = (int) $db->lastInsertId();
-
- $syncVariants($newId);
-
- return ['Product created.', 'success', $newId];
-