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:
parent
d0b9242b89
commit
4bb9ed86d4
1
.astro/types.d.ts
vendored
1
.astro/types.d.ts
vendored
@ -1,2 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference path="content.d.ts" />
|
||||
95
admin.mjs
95
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] },
|
||||
},
|
||||
},
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user