223 lines
6.6 KiB
JavaScript
223 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* stripe-sync.mjs
|
||
* ────────────────
|
||
* Synchronise les produits de la BDD vers Stripe :
|
||
* 1. Pour chaque produit avec un prix (price != null) :
|
||
* - Crée le Product Stripe (ou le retrouve via metadata.slug)
|
||
* - Crée le Price Stripe
|
||
* - Met à jour le stripePriceId dans la BDD
|
||
* 2. Les produits sans prix sont ignorés
|
||
*
|
||
* Usage :
|
||
* node scripts/stripe-sync.mjs # mode dry-run
|
||
* node scripts/stripe-sync.mjs --confirm # exécution réelle
|
||
*/
|
||
|
||
import 'dotenv/config'
|
||
import Stripe from 'stripe'
|
||
import { PrismaClient } from '@prisma/client'
|
||
|
||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY
|
||
if (!STRIPE_SECRET_KEY) {
|
||
console.error('❌ STRIPE_SECRET_KEY manquant dans .env')
|
||
process.exit(1)
|
||
}
|
||
|
||
const stripe = new Stripe(STRIPE_SECRET_KEY)
|
||
const prisma = new PrismaClient()
|
||
const dryRun = !process.argv.includes('--confirm')
|
||
|
||
if (dryRun) {
|
||
console.log('🔍 MODE DRY-RUN — rien ne sera créé ni modifié')
|
||
console.log(' Ajouter --confirm pour exécuter réellement\n')
|
||
}
|
||
|
||
/**
|
||
* Cherche un produit Stripe existant par metadata.slug
|
||
*/
|
||
async function findExistingStripeProduct(slug) {
|
||
let hasMore = true
|
||
let startingAfter = undefined
|
||
|
||
while (hasMore) {
|
||
const params = { limit: 100, active: true }
|
||
if (startingAfter) params.starting_after = startingAfter
|
||
|
||
const products = await stripe.products.list(params)
|
||
|
||
for (const product of products.data) {
|
||
if (product.metadata?.slug === slug) {
|
||
return product
|
||
}
|
||
}
|
||
|
||
hasMore = products.has_more
|
||
if (products.data.length > 0) {
|
||
startingAfter = products.data[products.data.length - 1].id
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* Cherche un prix actif existant pour un produit Stripe
|
||
* avec le bon montant et la bonne devise
|
||
*/
|
||
async function findExistingPrice(stripeProductId, amount, currency) {
|
||
const prices = await stripe.prices.list({
|
||
product: stripeProductId,
|
||
active: true,
|
||
limit: 100,
|
||
})
|
||
|
||
return prices.data.find(
|
||
p => p.unit_amount === amount && p.currency === currency.toLowerCase()
|
||
)
|
||
}
|
||
|
||
async function main() {
|
||
// Récupérer tous les produits publiés avec un prix
|
||
const products = await prisma.product.findMany({
|
||
where: {
|
||
isPublished: true,
|
||
price: { not: null },
|
||
},
|
||
orderBy: { sortOrder: 'asc' },
|
||
})
|
||
|
||
if (products.length === 0) {
|
||
console.log('ℹ️ Aucun produit avec un prix trouvé dans la BDD')
|
||
return
|
||
}
|
||
|
||
console.log(`📦 ${products.length} produit(s) à synchroniser vers Stripe\n`)
|
||
|
||
let created = 0
|
||
let updated = 0
|
||
let skipped = 0
|
||
|
||
for (const product of products) {
|
||
const priceCents = product.price
|
||
const currency = product.currency.toLowerCase()
|
||
|
||
console.log(`── ${product.productDisplayName} (${product.slug})`)
|
||
console.log(` Prix: ${priceCents / 100} ${currency.toUpperCase()}`)
|
||
|
||
// 1. Chercher ou créer le Product Stripe
|
||
let stripeProduct = await findExistingStripeProduct(product.slug)
|
||
|
||
if (stripeProduct) {
|
||
console.log(` ✓ Produit Stripe existant: ${stripeProduct.id}`)
|
||
|
||
if (!dryRun) {
|
||
// Mettre à jour les infos du produit
|
||
stripeProduct = await stripe.products.update(stripeProduct.id, {
|
||
name: product.productDisplayName,
|
||
description: product.description,
|
||
images: product.ogImage ? [product.ogImage] : undefined,
|
||
metadata: {
|
||
slug: product.slug,
|
||
stripeKey: product.stripeKey || product.slug,
|
||
dbId: product.id,
|
||
},
|
||
})
|
||
console.log(` ✓ Produit Stripe mis à jour`)
|
||
}
|
||
} else {
|
||
if (dryRun) {
|
||
console.log(` 📌 Produit Stripe SERAIT créé`)
|
||
} else {
|
||
stripeProduct = await stripe.products.create({
|
||
name: product.productDisplayName,
|
||
description: product.description,
|
||
images: product.ogImage ? [product.ogImage] : [],
|
||
metadata: {
|
||
slug: product.slug,
|
||
stripeKey: product.stripeKey || product.slug,
|
||
dbId: product.id,
|
||
},
|
||
})
|
||
console.log(` ✓ Produit Stripe créé: ${stripeProduct.id}`)
|
||
}
|
||
}
|
||
|
||
// 2. Chercher ou créer le Price Stripe
|
||
let stripePrice = null
|
||
|
||
if (stripeProduct) {
|
||
stripePrice = await findExistingPrice(stripeProduct.id, priceCents, currency)
|
||
}
|
||
|
||
if (stripePrice) {
|
||
console.log(` ✓ Prix Stripe existant: ${stripePrice.id} (${priceCents / 100} ${currency.toUpperCase()})`)
|
||
|
||
// Vérifier si le stripePriceId dans la BDD est à jour
|
||
if (product.stripePriceId === stripePrice.id) {
|
||
console.log(` ✓ BDD déjà à jour`)
|
||
skipped++
|
||
} else {
|
||
if (dryRun) {
|
||
console.log(` 📌 stripePriceId SERAIT mis à jour: ${product.stripePriceId || '(vide)'} → ${stripePrice.id}`)
|
||
updated++
|
||
} else {
|
||
await prisma.product.update({
|
||
where: { id: product.id },
|
||
data: { stripePriceId: stripePrice.id },
|
||
})
|
||
console.log(` ✓ BDD mise à jour: stripePriceId = ${stripePrice.id}`)
|
||
updated++
|
||
}
|
||
}
|
||
} else {
|
||
// Créer un nouveau prix
|
||
if (dryRun) {
|
||
console.log(` 📌 Prix Stripe SERAIT créé: ${priceCents / 100} ${currency.toUpperCase()}`)
|
||
console.log(` 📌 stripePriceId SERAIT mis à jour dans la BDD`)
|
||
created++
|
||
} else {
|
||
stripePrice = await stripe.prices.create({
|
||
product: stripeProduct.id,
|
||
unit_amount: priceCents,
|
||
currency: currency,
|
||
metadata: {
|
||
slug: product.slug,
|
||
dbId: product.id,
|
||
},
|
||
})
|
||
console.log(` ✓ Prix Stripe créé: ${stripePrice.id}`)
|
||
|
||
// 3. Mettre à jour le stripePriceId dans la BDD
|
||
await prisma.product.update({
|
||
where: { id: product.id },
|
||
data: { stripePriceId: stripePrice.id },
|
||
})
|
||
console.log(` ✓ BDD mise à jour: stripePriceId = ${stripePrice.id}`)
|
||
created++
|
||
}
|
||
}
|
||
|
||
console.log()
|
||
}
|
||
|
||
console.log('─'.repeat(50))
|
||
if (dryRun) {
|
||
console.log(`📊 Résumé (dry-run) :`)
|
||
console.log(` ${created} produit(s)/prix SERAIENT créés`)
|
||
console.log(` ${updated} stripePriceId SERAIENT mis à jour`)
|
||
console.log(` ${skipped} déjà synchronisé(s)`)
|
||
console.log(`\n👉 Relancer avec --confirm pour exécuter`)
|
||
} else {
|
||
console.log(`✅ Synchronisation terminée :`)
|
||
console.log(` ${created} créé(s), ${updated} mis à jour, ${skipped} inchangé(s)`)
|
||
}
|
||
}
|
||
|
||
main()
|
||
.catch(err => {
|
||
console.error('❌ Erreur:', err.message)
|
||
process.exit(1)
|
||
})
|
||
.finally(() => prisma.$disconnect())
|