3) Replace — routes/stripeWebhook.js
// routes/stripeWebhook.js
const express = require("express");
const Stripe = require("stripe");
const {
  grantLicenses,
  upsertQuotasFromStripeSubscription,
} = require("../services/entitlements");

const router = express.Router();

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: "2024-06-20",
});

router.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["stripe-signature"];
  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    console.error("Webhook signature verification failed.", err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  (async () => {
    switch (event.type) {
      case "checkout.session.completed": {
        const session = event.data.object;

        // One-off image purchases carry assetIds in metadata; grant permanent licenses
        const assetIds = (session.metadata?.assetIds || "")
          .split(",")
          .map((s) => s.trim())
          .filter(Boolean);

        const userId = session.metadata?.userId || session.client_reference_id || "anon";
        if (assetIds.length && userId !== "anon") {
          grantLicenses(userId, assetIds);
          console.log("✅ Granted licenses:", { userId, assetIds });
        } else {
          console.log("✅ Checkout complete (no assetIds to license).");
        }
        break;
      }

      case "customer.subscription.created":
      case "customer.subscription.updated":
      case "customer.subscription.deleted": {
        const subscription = event.data.object;

        // We need a user id. You can store Stripe customer -> user mapping in your DB.
        // For now, try to read from metadata if you set it at session or customer level.
        // Fallback to 'anon' (no quotas updated).
        const userId =
          subscription.metadata?.userId ||
          subscription.customer_email || // if you attached email and use it as user key
          "anon";

        if (userId !== "anon") {
          upsertQuotasFromStripeSubscription(userId, subscription);
          console.log(`ℹ️ Synced subscription quotas for ${userId} (${subscription.id})`);
        } else {
          console.warn("⚠️ Subscription event without userId mapping; quotas not updated.");
        }
        break;
      }

      default:
        // ignore unhandled events
        break;
    }
  })()
    .then(() => res.json({ received: true }))
    .catch((e) => {
      console.error("Webhook handler error", e);
      res.status(500).send("Webhook handler failed");
    });
});

module.exports = router;


Note: If you already map Stripe customer → your userId, replace the userId derivation above accordingly (best is your own DB mapping). If you pass customer_email that equals your login email, you can use that as the key.

4) Replace — routes/stockLibraryRoutes.js

This is your previous stock routes with license/quota checks added in GET /:id/download.
If you’ve made storage-path tweaks, copy those back in after pasting this (the quota/entitlement parts are clearly marked).

// routes/stockLibraryRoutes.js
// Upload + serve stock photos with server-side watermarking for previews/unlicensed
const path = require("path");
const fs = require("fs/promises");
const express = require("express");
const multer = require("multer");
const {
  applyWatermark,
  toTenDigitCode,
} = require("../services/watermark");
const ObjectStorageService = require("../services/ObjectStorageService"); // your existing wrapper

// NEW: entitlements
const {
  hasLicense,
  grantLicenses,
  getTotalRemaining,
  decrementOne,
} = require("../services/entitlements");

const router = express.Router();

// --- Multer storage (temp to local before pushing to object storage) ---
const uploadDir = path.join(process.cwd(), "uploads", "photos");
const storage = multer.diskStorage({
  destination: async (_req, _file, cb) => {
    await fs.mkdir(uploadDir, { recursive: true });
    cb(null, uploadDir);
  },
  filename: (_req, file, cb) => {
    const stamp = Date.now();
    const safe = file.originalname.replace(/\s+/g, "_");
    cb(null, `${stamp}_${safe}`);
  },
});
const upload = multer({
  storage,
  limits: { fileSize: 25 * 1024 * 1024 }, // 25MB
});

// Helper: object storage keys (adjust if you customized prefixes)
function objKeyOriginal(dbId, filename) {
  return `stock/original/${dbId}/${filename}`;
}
function objKeyPreview(dbId, filename) {
  return `stock/preview/${dbId}/${filename}`;
}

// Mock DB insert (replace with your real DB)
async function insertStockDBRecord({ originalName, mimeType, uploaderId }) {
  const id =
    Math.floor(Date.now() / 1000).toString(36) +
    Math.floor(Math.random() * 1e6).toString(36);
  return { id, originalName, mimeType, uploaderId };
}

// POST /api/stock/upload (admin)
router.post("/upload", upload.single("file"), async (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ error: "No file uploaded" });

    // 1) Create DB record
    const dbRow = await insertStockDBRecord({
      originalName: req.file.originalname,
      mimeType: req.file.mimetype,
      uploaderId: req.user?.id || "admin",
    });

    const tenDigit = toTenDigitCode(dbRow.id);

    // 2) Push ORIGINAL (private)
    const originalBuf = await fs.readFile(req.file.path);
    const originalKey = objKeyOriginal(dbRow.id, req.file.filename);
    await ObjectStorageService.putObject(originalKey, originalBuf, req.file.mimetype, { privacy: "private" });

    // 3) WATERMARKED PREVIEW (public)
    const wmBuf = await applyWatermark(originalBuf, { stockSeed: dbRow.id, opacity: 0.15 });
    const previewKey = objKeyPreview(dbRow.id, req.file.filename);
    await ObjectStorageService.putObject(previewKey, wmBuf, req.file.mimetype, { privacy: "public" });

    // 4) Clean local temp
    await fs.unlink(req.file.path).catch(() => {});

    // 5) Response metadata
    const previewUrl = await ObjectStorageService.getPublicUrl(previewKey);
    res.json({
      id: dbRow.id,
      code: tenDigit,
      name: req.file.originalname,
      mimeType: req.file.mimetype,
      previewUrl,
      originalKey, // private
    });
  } catch (err) {
    console.error("Upload error:", err);
    res.status(500).json({ error: "Upload failed" });
  }
});

// GET /api/stock/:id/preview (always public, watermarked)
router.get("/:id/preview", async (req, res) => {
  try {
    const { id } = req.params;
    const list = await ObjectStorageService.listPrefix(`stock/preview/${id}/`);
    if (!list.length) return res.status(404).send("Not found");
    const key = list[0].key;
    const stream = await ObjectStorageService.getStream(key);
    res.setHeader("Content-Type", list[0].contentType || "image/jpeg");
    stream.pipe(res);
  } catch (err) {
    console.error(err);
    res.status(500).send("Preview error");
  }
});

// GET /api/stock/:id/download
// If user has license -> serve original.
// Else if user has quota -> grant license, decrement quota, serve original.
// Else -> serve watermarked preview (blocks original).
router.get("/:id/download", async (req, res) => {
  try {
    const { id } = req.params;
    const userId = req.user?.id || null;

    // Helper: stream original by first key under /stock/original/{id}/
    async function streamOriginal() {
      const listO = await ObjectStorageService.listPrefix(`stock/original/${id}/`);
      if (!listO.length) return res.status(404).send("Original not found");
      const keyO = listO[0].key;
      const streamO = await ObjectStorageService.getStream(keyO);
      res.setHeader("Content-Disposition", `attachment; filename="IBrandBiz_${id}.jpg"`);
      res.setHeader("Content-Type", listO[0].contentType || "image/jpeg");
      return streamO.pipe(res);
    }

    // Helper: stream preview (watermarked)
    async function streamPreviewWithHeaders(extraHeaders = {}) {
      const list = await ObjectStorageService.listPrefix(`stock/preview/${id}/`);
      if (!list.length) return res.status(404).send("Preview not found");
      const key = list[0].key;
      const stream = await ObjectStorageService.getStream(key);
      for (const [k, v] of Object.entries(extraHeaders)) res.setHeader(k, v);
      res.setHeader("Content-Type", list[0].contentType || "image/jpeg");
      return stream.pipe(res);
    }

    // Anonymous users never get original
    if (!userId) {
      return streamPreviewWithHeaders({ "X-Quota-Remaining": "0", "X-License": "none" });
    }

    // 1) If already licensed → original
    if (hasLicense(userId, id)) {
      res.setHeader("X-License", "owned");
      return streamOriginal();
    }

    // 2) Not licensed → try to burn a quota
    const remaining = getTotalRemaining(userId);
    if (remaining > 0) {
      const ok = decrementOne(userId);
      if (ok) {
        // Grant a permanent license for this asset so repeat downloads don't burn quota again.
        grantLicenses(userId, [id]);
        res.setHeader("X-Quota-Remaining", String(remaining - 1));
        res.setHeader("X-License", "newly-granted");
        return streamOriginal();
      }
    }

    // 3) No license + no quota → serve preview (block original)
    return streamPreviewWithHeaders({ "X-Quota-Remaining": "0", "X-License": "none" });
  } catch (err) {
    console.error(err);
    res.status(500).send("Download error");
  }
});

module.exports = router;
