feat: auto-sync product prices to Stripe from admin + UI fixes

- Admin edit hook syncs prices to Stripe (create/archive)
- "Prochainement disponible" disabled button for products without price
- Seed no longer hardcodes stripePriceId
- Fix nginx port mismatch (3001 → 3000)
- Coord tag background color + sound auto-start fix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-03-20 23:30:39 +01:00
parent d0b9242b89
commit 4bb9ed86d4
6 changed files with 115 additions and 12 deletions

1
.astro/types.d.ts vendored
View File

@ -1,2 +1 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

View File

@ -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] },
},
},

View File

@ -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;

View File

@ -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,
},

View File

@ -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 {

View File

@ -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();