diff --git a/.astro/types.d.ts b/.astro/types.d.ts index 03d7cc4..f964fe0 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1,2 +1 @@ /// -/// \ No newline at end of file diff --git a/admin.mjs b/admin.mjs index 1003082..ad8c06a 100644 --- a/admin.mjs +++ b/admin.mjs @@ -2,11 +2,16 @@ import AdminJS from 'adminjs' import AdminJSFastify from '@adminjs/fastify' import { Database, Resource, getModelByName } from '@adminjs/prisma' import bcrypt from 'bcrypt' +import Stripe from 'stripe' import { exec } from 'child_process' import { prisma } from './src/lib/db.mjs' AdminJS.registerAdapter({ Database, Resource }) +const stripe = process.env.STRIPE_SECRET_KEY + ? new Stripe(process.env.STRIPE_SECRET_KEY) + : null + // ── Auto-build (prod only) ────────────────────────────────────────────────── let buildTimeout = null let buildInProgress = false @@ -31,6 +36,92 @@ function triggerBuild() { const afterBuildHook = async (response) => { triggerBuild(); return response } +// ── Stripe price sync after product edit ───────────────────────────────────── +async function syncPriceToStripe(response) { + if (!stripe) return response + + const product = await prisma.product.findUnique({ + where: { id: response.record.params.id }, + }) + if (!product) return response + + const priceCents = product.price + const currency = (product.currency || 'EUR').toLowerCase() + const slug = product.slug + + try { + // 1. Find or create Stripe product by metadata.slug + let stripeProduct = null + const existing = await stripe.products.search({ query: `metadata["slug"]:"${slug}"` }) + if (existing.data.length > 0) { + stripeProduct = existing.data[0] + // Update product info + await stripe.products.update(stripeProduct.id, { + name: product.productDisplayName, + description: product.description, + images: product.ogImage ? [product.ogImage] : undefined, + active: true, + }) + } else if (priceCents) { + stripeProduct = await stripe.products.create({ + name: product.productDisplayName, + description: product.description, + images: product.ogImage ? [product.ogImage] : [], + metadata: { slug, stripeKey: product.stripeKey || slug, dbId: product.id }, + }) + console.log(`[stripe] Product created: ${stripeProduct.id}`) + } + + if (!stripeProduct) return response + + // 2. If price is null, archive old price and clear DB + if (!priceCents) { + if (product.stripePriceId) { + await stripe.prices.update(product.stripePriceId, { active: false }) + await prisma.product.update({ where: { id: product.id }, data: { stripePriceId: null } }) + console.log(`[stripe] Price archived (product has no price now)`) + } + return response + } + + // 3. Check if current Stripe price matches + if (product.stripePriceId) { + try { + const currentPrice = await stripe.prices.retrieve(product.stripePriceId) + if (currentPrice.unit_amount === priceCents && currentPrice.currency === currency && currentPrice.active) { + // Price unchanged + return response + } + // Archive old price + await stripe.prices.update(product.stripePriceId, { active: false }) + console.log(`[stripe] Old price archived: ${product.stripePriceId}`) + } catch { + // Old price doesn't exist, continue + } + } + + // 4. Create new price + const newPrice = await stripe.prices.create({ + product: stripeProduct.id, + unit_amount: priceCents, + currency, + metadata: { slug, dbId: product.id }, + }) + console.log(`[stripe] New price created: ${newPrice.id} (${priceCents / 100} ${currency.toUpperCase()})`) + + // 5. Update DB + await prisma.product.update({ + where: { id: product.id }, + data: { stripePriceId: newPrice.id }, + }) + + } catch (err) { + console.error(`[stripe] Sync error for ${slug}:`, err.message) + } + + return response +} + // ── AdminJS setup ─────────────────────────────────────────────────────────── export async function setupAdmin(app) { const admin = new AdminJS({ @@ -56,8 +147,8 @@ export async function setupAdmin(app) { seoDescription: { type: 'textarea' }, }, actions: { - new: { after: [afterBuildHook] }, - edit: { after: [afterBuildHook] }, + new: { after: [syncPriceToStripe, afterBuildHook] }, + edit: { after: [syncPriceToStripe, afterBuildHook] }, delete: { after: [afterBuildHook] }, }, }, diff --git a/nginx.conf b/nginx.conf index 64316af..edd2acc 100644 --- a/nginx.conf +++ b/nginx.conf @@ -7,7 +7,7 @@ server { # ── API proxy → Fastify ────────────────────────────────────────────────── location /api/ { - proxy_pass http://127.0.0.1:3001; + proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -16,20 +16,20 @@ server { # ── SEO (dynamique depuis DB) ──────────────────────────────────────────── location = /robots.txt { - proxy_pass http://127.0.0.1:3001; + proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location = /sitemap.xml { - proxy_pass http://127.0.0.1:3001; + proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # ── Admin proxy → Fastify (AdminJS) ────────────────────────────────────── location ^~ /admin { - proxy_pass http://127.0.0.1:3001; + proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/prisma/seed.mjs b/prisma/seed.mjs index 7c216ae..7210e0e 100644 --- a/prisma/seed.mjs +++ b/prisma/seed.mjs @@ -25,7 +25,7 @@ const PRODUCTS = [ price: 180000, // 1800 EUR in cents currency: 'EUR', availability: 'https://schema.org/LimitedAvailability', - stripePriceId: 'price_1T5SBlE5wMMoCUP5ZcjEStwe', + stripePriceId: null, stripeKey: 'lumiere_orbitale', isPublished: true, }, diff --git a/public/style.css b/public/style.css index 020ff3e..fd4088b 100644 --- a/public/style.css +++ b/public/style.css @@ -470,10 +470,17 @@ hr { border: none; border-top: var(--border); margin: 0; } transition: background 0.15s, color 0.15s; pointer-events: auto; } -.checkout-btn:hover { +.checkout-btn:hover:not(:disabled) { background: var(--clr-black); color: #e8a800; } +.checkout-btn--disabled { + background: #999; + border-color: #999; + color: #ddd; + cursor: default; + pointer-events: none; +} /* Form qui se déploie */ .checkout-form-wrap { diff --git a/src/scripts/main.js b/src/scripts/main.js index 1ed09df..4e766c4 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -526,17 +526,23 @@ document.addEventListener('DOMContentLoaded', () => { const stripeKey = card.dataset.stripeKey; const isOrderable = price && stripeKey; + checkoutSection.style.display = 'block'; + if (isOrderable) { currentStripeKey = stripeKey; checkoutPriceEl.textContent = formatPrice(parseInt(price, 10)); - checkoutSection.style.display = 'block'; + checkoutToggleBtn.textContent = '[ COMMANDER CETTE PIÈCE ]'; + checkoutToggleBtn.disabled = false; + checkoutToggleBtn.classList.remove('checkout-btn--disabled'); } else { currentStripeKey = null; - checkoutSection.style.display = 'none'; + checkoutPriceEl.textContent = ''; + checkoutToggleBtn.textContent = 'PROCHAINEMENT DISPONIBLE'; + checkoutToggleBtn.disabled = true; + checkoutToggleBtn.classList.add('checkout-btn--disabled'); } checkoutFormWrap.style.display = 'none'; - checkoutToggleBtn.textContent = '[ COMMANDER CETTE PIÈCE ]'; checkoutSubmitBtn.disabled = false; checkoutSubmitBtn.textContent = 'PROCÉDER AU PAIEMENT →'; checkoutForm.reset();