@salifbiaye/create-boutique 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/dist/code.js +110 -0
- package/cli/dist/commands.js +145 -11
- package/cli/dist/create.js +54 -7
- package/cli/dist/generate.js +65 -4
- package/cli/dist/guide.js +10 -2
- package/cli/dist/index.js +20 -7
- package/cli/dist/payments.js +150 -0
- package/docs/fiche-client.en.html +2 -2
- package/docs/fiche-client.html +2 -2
- package/docs/guide-complet.fr.html +681 -256
- package/docs/guide-images/categories.jpg +0 -0
- package/docs/guide-images/clients.jpg +0 -0
- package/docs/guide-images/collections.jpg +0 -0
- package/docs/guide-images/commande-detail.jpg +0 -0
- package/docs/guide-images/commande-expedier.jpg +0 -0
- package/docs/guide-images/commande-menu.jpg +0 -0
- package/docs/guide-images/commande-paiement.jpg +0 -0
- package/docs/guide-images/commandes-liste.jpg +0 -0
- package/docs/guide-images/connexion.jpg +0 -0
- package/docs/guide-images/groupes-clients.jpg +0 -0
- package/docs/guide-images/inventaire.jpg +0 -0
- package/docs/guide-images/listes-prix.jpg +0 -0
- package/docs/guide-images/menu.jpg +0 -0
- package/docs/guide-images/mes-boutiques-fermer.jpg +0 -0
- package/docs/guide-images/mes-boutiques-panneau.jpg +0 -0
- package/docs/guide-images/mes-boutiques.jpg +0 -0
- package/docs/guide-images/parametres-boutique.jpg +0 -0
- package/docs/guide-images/parametres-canaux.jpg +0 -0
- package/docs/guide-images/parametres-emplacements.jpg +0 -0
- package/docs/guide-images/parametres-profil.jpg +0 -0
- package/docs/guide-images/parametres-regions.jpg +0 -0
- package/docs/guide-images/parametres-utilisateurs.jpg +0 -0
- package/docs/guide-images/produit-creer-details.jpg +0 -0
- package/docs/guide-images/produit-detail.jpg +0 -0
- package/docs/guide-images/produit-variantes-menu.jpg +0 -0
- package/docs/guide-images/produit-variantes.jpg +0 -0
- package/docs/guide-images/produits-liste.jpg +0 -0
- package/docs/guide-images/promotion-details.jpg +0 -0
- package/docs/guide-images/promotion-type.jpg +0 -0
- package/docs/guide-images/promotions-liste.jpg +0 -0
- package/docs/guide-images/site-boutique.jpg +0 -0
- package/docs/guide-images/site-fermee.png +0 -0
- package/docs/guide-images/vue-ensemble.jpg +0 -0
- package/package.json +3 -2
- package/templates/backend/medusa-config.ts +31 -25
- package/templates/backend/package-lock.json +21 -0
- package/templates/backend/package.json +2 -0
- package/templates/backend/src/admin/i18n/json/en.json +51 -3
- package/templates/backend/src/admin/i18n/json/fr.json +51 -3
- package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +2 -0
- package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +36 -1
- package/templates/backend/src/admin/routes/reviews/page.tsx +259 -0
- package/templates/backend/src/api/admin/boutique-campaigns/route.ts +3 -1
- package/templates/backend/src/api/admin/boutique-online/[salesChannelId]/route.ts +22 -0
- package/templates/backend/src/api/admin/reviews/[id]/route.ts +15 -0
- package/templates/backend/src/api/admin/reviews/route.ts +47 -0
- package/templates/backend/src/api/middlewares.ts +7 -0
- package/templates/backend/src/api/store/campaign/route.ts +5 -1
- package/templates/backend/src/api/store/reviews/latest/route.ts +39 -0
- package/templates/backend/src/api/store/reviews/order/route.ts +36 -0
- package/templates/backend/src/api/store/reviews/route.ts +43 -0
- package/templates/backend/src/api/store/reviews/summary/route.ts +28 -0
- package/templates/backend/src/emails/en.json +3 -1
- package/templates/backend/src/emails/fr.json +3 -1
- package/templates/backend/src/lib/email-config.ts +14 -0
- package/templates/backend/src/lib/emails.ts +9 -0
- package/templates/backend/src/lib/notify.ts +14 -8
- package/templates/backend/src/lib/payment-config.ts +66 -0
- package/templates/backend/src/lib/project.ts +12 -0
- package/templates/backend/src/lib/redirect-payment.ts +264 -0
- package/templates/backend/src/lib/review-token.ts +40 -0
- package/templates/backend/src/lib/reviews.ts +25 -0
- package/templates/backend/src/modules/naboopay/index.ts +6 -0
- package/templates/backend/src/modules/naboopay/service.ts +92 -0
- package/templates/backend/src/modules/paydunya/index.ts +6 -0
- package/templates/backend/src/modules/paydunya/service.ts +89 -0
- package/templates/backend/src/modules/paytech/index.ts +6 -0
- package/templates/backend/src/modules/paytech/service.ts +114 -0
- package/templates/backend/src/modules/product-review/index.ts +8 -0
- package/templates/backend/src/modules/product-review/migrations/Migration20261001090000.ts +18 -0
- package/templates/backend/src/modules/product-review/models/review.ts +25 -0
- package/templates/backend/src/modules/product-review/service.ts +6 -0
- package/templates/backend/src/modules/product-review/validators.ts +27 -0
- package/templates/backend/src/modules/smtp/index.ts +6 -0
- package/templates/backend/src/modules/smtp/service.ts +73 -0
- package/templates/backend/src/scripts/seed.ts +52 -36
- package/templates/backend/src/subscribers/fulfillment-emails.ts +1 -1
- package/templates/backend/src/workflows/reviews.ts +21 -0
- package/templates/backend/src/workflows/steps/create-review.ts +52 -0
- package/templates/backend/src/workflows/steps/update-review.ts +36 -0
- package/templates/storefront/PERSONNALISER.md +134 -0
- package/templates/storefront/locales/en.json +198 -11
- package/templates/storefront/locales/fr.json +198 -11
- package/templates/storefront/public/custom/LISEZMOI.txt +2 -0
- package/templates/storefront/src/app/(checkout)/layout.tsx +1 -1
- package/templates/storefront/src/app/(main)/layout.tsx +8 -1
- package/templates/storefront/src/app/(main)/legal/[page]/page.tsx +42 -7
- package/templates/storefront/src/app/(main)/page.tsx +149 -4
- package/templates/storefront/src/app/(main)/review/page.tsx +60 -0
- package/templates/storefront/src/app/(main)/store/page.tsx +3 -1
- package/templates/storefront/src/app/(main)/template.tsx +17 -2
- package/templates/storefront/src/app/layout.tsx +13 -1
- package/templates/storefront/src/lib/boutique/campaign.ts +5 -1
- package/templates/storefront/src/lib/boutique/page-name.ts +25 -0
- package/templates/storefront/src/lib/boutique/schema.ts +3 -1
- package/templates/storefront/src/lib/constants.tsx +17 -0
- package/templates/storefront/src/lib/data/review-actions.ts +28 -0
- package/templates/storefront/src/lib/data/reviews.ts +64 -0
- package/templates/storefront/src/modules/checkout/components/payment/index.tsx +26 -4
- package/templates/storefront/src/modules/checkout/components/payment-button/index.tsx +85 -2
- package/templates/storefront/src/modules/products/templates/index.tsx +5 -0
- package/templates/storefront/src/modules/store/components/shop-search.tsx +85 -0
- package/templates/storefront/src/modules/store/templates/index.tsx +16 -8
- package/templates/storefront/src/modules/store/templates/paginated-products.tsx +15 -3
- package/templates/storefront/src/modules/ub/album.tsx +2 -0
- package/templates/storefront/src/modules/ub/arena.tsx +182 -0
- package/templates/storefront/src/modules/ub/campaign-blocks.tsx +3 -2
- package/templates/storefront/src/modules/ub/campaign-ended.tsx +27 -13
- package/templates/storefront/src/modules/ub/cinema.tsx +2 -0
- package/templates/storefront/src/modules/ub/cookie-banner.tsx +62 -0
- package/templates/storefront/src/modules/ub/gala.tsx +2 -0
- package/templates/storefront/src/modules/ub/grimoire.tsx +35 -8
- package/templates/storefront/src/modules/ub/harvest.tsx +2 -1
- package/templates/storefront/src/modules/ub/hero-image.tsx +14 -0
- package/templates/storefront/src/modules/ub/home-kit.ts +45 -0
- package/templates/storefront/src/modules/ub/lantern.tsx +244 -0
- package/templates/storefront/src/modules/ub/locker.tsx +180 -0
- package/templates/storefront/src/modules/ub/mosaic.tsx +189 -0
- package/templates/storefront/src/modules/ub/nav.tsx +8 -5
- package/templates/storefront/src/modules/ub/review-form.tsx +62 -0
- package/templates/storefront/src/modules/ub/reviews.tsx +131 -0
- package/templates/storefront/src/modules/ub/riviera.tsx +175 -0
- package/templates/storefront/src/modules/ub/souk.tsx +214 -0
- package/templates/storefront/src/modules/ub/sunset-checklist.tsx +95 -0
- package/templates/storefront/src/modules/ub/sunset.tsx +164 -0
- package/templates/storefront/src/modules/ub/tech.tsx +2 -2
- package/templates/storefront/src/modules/ub/training.tsx +196 -0
- package/templates/storefront/src/proxy.ts +2 -0
- package/templates/storefront/src/styles/custom.css +26 -0
- package/templates/storefront/src/styles/ub/album.css +1 -1
- package/templates/storefront/src/styles/ub/arena.css +120 -0
- package/templates/storefront/src/styles/ub/cinema.css +1 -1
- package/templates/storefront/src/styles/ub/gala.css +1 -1
- package/templates/storefront/src/styles/ub/grimoire.css +23 -7
- package/templates/storefront/src/styles/ub/harvest.css +7 -6
- package/templates/storefront/src/styles/ub/lantern.css +157 -0
- package/templates/storefront/src/styles/ub/locker.css +115 -0
- package/templates/storefront/src/styles/ub/media.css +10 -0
- package/templates/storefront/src/styles/ub/mosaic.css +118 -0
- package/templates/storefront/src/styles/ub/motion.css +0 -3
- package/templates/storefront/src/styles/ub/overrides.css +10 -0
- package/templates/storefront/src/styles/ub/pages.css +176 -0
- package/templates/storefront/src/styles/ub/reviews.css +85 -0
- package/templates/storefront/src/styles/ub/riviera.css +103 -0
- package/templates/storefront/src/styles/ub/souk.css +144 -0
- package/templates/storefront/src/styles/ub/sunset.css +123 -0
- package/templates/storefront/src/styles/ub/tech.css +2 -5
- package/templates/storefront/src/styles/ub/training.css +120 -0
- package/templates/storefront/themes/jeux/campaign.en.json +31 -0
- package/templates/storefront/themes/jeux/campaign.fr.json +31 -0
- package/templates/storefront/themes/jeux/theme.json +11 -0
- package/templates/storefront/themes/jeux-2/campaign.en.json +31 -0
- package/templates/storefront/themes/jeux-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/jeux-2/theme.json +11 -0
- package/templates/storefront/themes/mubarak/campaign.en.json +31 -0
- package/templates/storefront/themes/mubarak/campaign.fr.json +31 -0
- package/templates/storefront/themes/mubarak/theme.json +11 -0
- package/templates/storefront/themes/mubarak-2/campaign.en.json +31 -0
- package/templates/storefront/themes/mubarak-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/mubarak-2/theme.json +11 -0
- package/templates/storefront/themes/sport/campaign.en.json +31 -0
- package/templates/storefront/themes/sport/campaign.fr.json +31 -0
- package/templates/storefront/themes/sport/theme.json +11 -0
- package/templates/storefront/themes/sport-2/campaign.en.json +31 -0
- package/templates/storefront/themes/sport-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/sport-2/theme.json +11 -0
- package/templates/storefront/themes/summer/campaign.en.json +31 -0
- package/templates/storefront/themes/summer/campaign.fr.json +31 -0
- package/templates/storefront/themes/summer/theme.json +11 -0
- package/templates/storefront/themes/summer-2/campaign.en.json +31 -0
- package/templates/storefront/themes/summer-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/summer-2/theme.json +11 -0
package/cli/dist/code.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { shouldCopy } from "./paths.js";
|
|
5
|
+
/**
|
|
6
|
+
* Code généré d'un projet (storefront/, backend/) :
|
|
7
|
+
* - fichiers conservés par `update` (retouches du projet) ;
|
|
8
|
+
* - empreinte de chaque fichier à la génération, pour détecter une modification faite à la main
|
|
9
|
+
* avant qu'`update` ne remplace le code (rien n'est plus jamais perdu sans prévenir).
|
|
10
|
+
*/
|
|
11
|
+
/** Chemins (relatifs au projet) que `update` ne remplace jamais. */
|
|
12
|
+
export const KEEP = ["storefront/src/styles/custom.css", "storefront/public/custom"];
|
|
13
|
+
const PARTS = ["storefront", "backend"];
|
|
14
|
+
const MANIFEST = (dir) => path.join(dir, ".create-boutique", "code.json");
|
|
15
|
+
const kept = (rel) => KEEP.some((k) => rel === k || rel.startsWith(`${k}/`));
|
|
16
|
+
function walk(dir, root, out) {
|
|
17
|
+
if (!fs.existsSync(dir))
|
|
18
|
+
return;
|
|
19
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
20
|
+
if (!shouldCopy(e.name))
|
|
21
|
+
continue;
|
|
22
|
+
const abs = path.join(dir, e.name);
|
|
23
|
+
const rel = path.relative(root, abs).split(path.sep).join("/");
|
|
24
|
+
if (kept(rel))
|
|
25
|
+
continue;
|
|
26
|
+
if (e.isDirectory())
|
|
27
|
+
walk(abs, root, out);
|
|
28
|
+
else
|
|
29
|
+
out.set(rel, crypto.createHash("sha256").update(fs.readFileSync(abs)).digest("hex"));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function hashes(target) {
|
|
33
|
+
const out = new Map();
|
|
34
|
+
for (const part of PARTS)
|
|
35
|
+
walk(path.join(target, part), target, out);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Enregistre l'empreinte du code tel que généré (après create et update). */
|
|
39
|
+
export function writeFingerprint(target, version) {
|
|
40
|
+
fs.mkdirSync(path.dirname(MANIFEST(target)), { recursive: true });
|
|
41
|
+
fs.writeFileSync(MANIFEST(target), JSON.stringify({ version, files: Object.fromEntries(hashes(target)) }, null, 2) + "\n");
|
|
42
|
+
}
|
|
43
|
+
/** Fichiers modifiés, ajoutés ou supprimés à la main depuis la génération (known=false : projet sans empreinte). */
|
|
44
|
+
export function localChanges(target) {
|
|
45
|
+
if (!fs.existsSync(MANIFEST(target)))
|
|
46
|
+
return { known: false, modified: [], added: [], deleted: [] };
|
|
47
|
+
const before = new Map(Object.entries(JSON.parse(fs.readFileSync(MANIFEST(target), "utf8")).files));
|
|
48
|
+
const now = hashes(target);
|
|
49
|
+
const modified = [...now].filter(([f, h]) => before.has(f) && before.get(f) !== h).map(([f]) => f);
|
|
50
|
+
const added = [...now.keys()].filter((f) => !before.has(f));
|
|
51
|
+
const deleted = [...before.keys()].filter((f) => !now.has(f));
|
|
52
|
+
return { known: true, modified, added, deleted };
|
|
53
|
+
}
|
|
54
|
+
/** Copie des fichiers (chemins relatifs au projet) dans un dossier de sauvegarde, arborescence conservée. */
|
|
55
|
+
export function copyFiles(target, files, to) {
|
|
56
|
+
for (const rel of files) {
|
|
57
|
+
const src = path.join(target, rel);
|
|
58
|
+
if (!fs.existsSync(src))
|
|
59
|
+
continue;
|
|
60
|
+
const dst = path.join(to, rel);
|
|
61
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
62
|
+
fs.copyFileSync(src, dst);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Tous les fichiers du code (projet sans empreinte : on sauvegarde tout, par précaution). */
|
|
66
|
+
export const allCodeFiles = (target) => [...hashes(target).keys()];
|
|
67
|
+
/** Met de côté les chemins conservés, pour les remettre après le remplacement du code. */
|
|
68
|
+
export function stashKept(target, tmp) {
|
|
69
|
+
const saved = [];
|
|
70
|
+
for (const rel of KEEP) {
|
|
71
|
+
const src = path.join(target, rel);
|
|
72
|
+
if (!fs.existsSync(src))
|
|
73
|
+
continue;
|
|
74
|
+
fs.cpSync(src, path.join(tmp, rel), { recursive: true });
|
|
75
|
+
saved.push(rel);
|
|
76
|
+
}
|
|
77
|
+
return saved;
|
|
78
|
+
}
|
|
79
|
+
export function restoreKept(target, tmp, saved) {
|
|
80
|
+
for (const rel of saved) {
|
|
81
|
+
const dst = path.join(target, rel);
|
|
82
|
+
fs.rmSync(dst, { recursive: true, force: true });
|
|
83
|
+
fs.cpSync(path.join(tmp, rel), dst, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* custom.css : une section prête à remplir par boutique du projet (ajoutée seulement si elle manque :
|
|
89
|
+
* ce que le développeur a écrit n'est jamais modifié). Appelé à la création et à chaque apply (ex. add).
|
|
90
|
+
*/
|
|
91
|
+
export function syncCustomCss(target, boutiques) {
|
|
92
|
+
const file = path.join(target, KEEP[0]);
|
|
93
|
+
if (!fs.existsSync(file))
|
|
94
|
+
return;
|
|
95
|
+
let css = fs.readFileSync(file, "utf8");
|
|
96
|
+
// Projets créés avant le mode d'emploi : une ligne de renvoi en tête (le reste du fichier n'est pas touché)
|
|
97
|
+
const pointer = "/* Mode d'emploi complet (plan du site, classes, [data-page], exemples) : storefront/PERSONNALISER.md */\n";
|
|
98
|
+
const withPointer = css.includes("PERSONNALISER.md") ? css : pointer + css;
|
|
99
|
+
const missing = boutiques.filter((b) => !css.includes(`[data-boutique="${b.slug}"]`));
|
|
100
|
+
if (!missing.length) {
|
|
101
|
+
if (withPointer !== css)
|
|
102
|
+
fs.writeFileSync(file, withPointer);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
css = withPointer;
|
|
106
|
+
for (const b of missing) {
|
|
107
|
+
css += `\n/* ─────────── Boutique ${b.slug} · thème ${b.theme} · ${b.url} ─────────── */\n[data-boutique="${b.slug}"] {\n /* ex. --primary: #4a2bd6; */\n}\n`;
|
|
108
|
+
}
|
|
109
|
+
fs.writeFileSync(file, css);
|
|
110
|
+
}
|
package/cli/dist/commands.js
CHANGED
|
@@ -5,6 +5,8 @@ import pc from "picocolors";
|
|
|
5
5
|
import { compose, composeOutput, isRunning } from "./docker.js";
|
|
6
6
|
import { readEnv, refreshEnv, writeCampaignFiles, writeCompose, writeReadme } from "./generate.js";
|
|
7
7
|
import { copyTemplate, templateVersion } from "./paths.js";
|
|
8
|
+
import { KEEP, allCodeFiles, copyFiles, syncCustomCss, localChanges, restoreKept, stashKept, writeFingerprint } from "./code.js";
|
|
9
|
+
import * as prompts from "@clack/prompts";
|
|
8
10
|
import { nextFreePort, reservedByOthers, reservePorts } from "./ports.js";
|
|
9
11
|
import { boutiqueUrl, currencies, effective, normalizeUrl, readProject, validateProject, writeJsonSchema, writeProject, ProjectError, SLUG, storePorts } from "./project.js";
|
|
10
12
|
import { listThemes } from "./themes.js";
|
|
@@ -63,6 +65,7 @@ export async function apply(dir, opts = { up: true }) {
|
|
|
63
65
|
if (clash.length)
|
|
64
66
|
throw new ProjectError(`port(s) déjà réservé(s) par un autre projet : ${clash.join(", ")}`);
|
|
65
67
|
writeCampaignFiles(target, p, p.defaults.language);
|
|
68
|
+
syncCustomCss(target, p.boutiques.map((b) => ({ slug: b.slug, theme: b.theme, url: boutiqueUrl(p, b) })));
|
|
66
69
|
writeJsonSchema(target);
|
|
67
70
|
writeCompose(target, p);
|
|
68
71
|
refreshEnv(target, p);
|
|
@@ -248,29 +251,90 @@ export function themes() {
|
|
|
248
251
|
console.log(`${t.id.padEnd(16)} ${t.name.padEnd(26)} langues : ${t.languages.join(", ")}`);
|
|
249
252
|
}
|
|
250
253
|
// ─────────────────────────── update ───────────────────────────
|
|
251
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* Remplace storefront/ et backend/ par la version actuelle du template (config, seed, .env et données conservés).
|
|
256
|
+
* Conservés aussi : les retouches du projet (KEEP : custom.css, public/custom/).
|
|
257
|
+
* Code modifié à la main depuis la génération : listé, copié dans backups/<date>-code/, et confirmation demandée.
|
|
258
|
+
*/
|
|
252
259
|
export async function update(dir, opts) {
|
|
253
260
|
const target = path.resolve(dir);
|
|
254
261
|
const p = readProject(target);
|
|
255
262
|
const from = p.templateVersion ?? "?";
|
|
256
263
|
const to = templateVersion();
|
|
264
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
265
|
+
const saveDir = path.join(target, "backups", `${stamp}-code`);
|
|
266
|
+
// Sauvegarde des données avant de changer le code (les migrations d'une nouvelle version touchent la base)
|
|
267
|
+
if (isRunning(target, "postgres"))
|
|
268
|
+
backup(target, { label: "avant-update" });
|
|
269
|
+
else
|
|
270
|
+
console.log(pc.yellow("! base arrêtée : pas de sauvegarde automatique avant la mise à jour"));
|
|
271
|
+
const changes = localChanges(target);
|
|
272
|
+
if (!changes.known) {
|
|
273
|
+
// Projet généré avant les empreintes : impossible de savoir ce qui a été modifié, on garde une copie de tout le code
|
|
274
|
+
copyFiles(target, allCodeFiles(target), saveDir);
|
|
275
|
+
console.log(pc.yellow(`! projet sans empreinte du code : copie complète de storefront/ et backend/ dans ${path.relative(target, saveDir)}/ (à comparer si tu avais modifié le code)`));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
const touched = [...changes.modified, ...changes.added];
|
|
279
|
+
if (touched.length || changes.deleted.length) {
|
|
280
|
+
const list = [
|
|
281
|
+
...changes.modified.map((f) => ` modifié ${f}`),
|
|
282
|
+
...changes.added.map((f) => ` ajouté ${f}`),
|
|
283
|
+
...changes.deleted.map((f) => ` supprimé ${f}`),
|
|
284
|
+
];
|
|
285
|
+
console.log(pc.yellow(`\n! Code modifié à la main depuis la génération (${list.length} fichier(s)) : update va le remplacer.`));
|
|
286
|
+
console.log(list.slice(0, 40).join("\n") + (list.length > 40 ? `\n … et ${list.length - 40} autre(s)` : ""));
|
|
287
|
+
console.log(` Pour garder une retouche après les mises à jour : la mettre dans ${KEEP.join(" ou ")}.\n`);
|
|
288
|
+
if (!opts.yes) {
|
|
289
|
+
if (!process.stdin.isTTY)
|
|
290
|
+
throw new ProjectError("code modifié à la main : relancez avec --yes pour mettre à jour quand même (une copie sera faite dans backups/)");
|
|
291
|
+
const ok = await prompts.confirm({ message: `Mettre à jour quand même ? (copie de ces fichiers dans backups/${stamp}-code/)`, initialValue: false });
|
|
292
|
+
if (prompts.isCancel(ok) || !ok)
|
|
293
|
+
throw new ProjectError("mise à jour annulée : rien n'a été modifié");
|
|
294
|
+
}
|
|
295
|
+
copyFiles(target, touched, saveDir);
|
|
296
|
+
console.log(pc.green(`✔ fichiers modifiés copiés dans ${path.relative(target, saveDir)}/`));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const tmp = path.join(target, ".create-boutique", "keep-tmp");
|
|
300
|
+
const saved = stashKept(target, tmp);
|
|
257
301
|
for (const part of ["storefront", "backend"]) {
|
|
258
302
|
fs.rmSync(path.join(target, part), { recursive: true, force: true });
|
|
259
303
|
copyTemplate(part, target);
|
|
260
304
|
}
|
|
305
|
+
restoreKept(target, tmp, saved);
|
|
306
|
+
writeFingerprint(target, to);
|
|
261
307
|
p.templateVersion = to;
|
|
262
308
|
writeProject(target, p);
|
|
263
|
-
console.log(pc.green(`✔ code mis à jour : template ${from} → ${to}`));
|
|
309
|
+
console.log(pc.green(`✔ code mis à jour : template ${from} → ${to}${saved.length ? ` (conservés : ${saved.join(", ")})` : ""}`));
|
|
264
310
|
await apply(target, { up: false });
|
|
265
311
|
if (opts.up && compose(target, ["up", "-d", "--build", "--remove-orphans"]) !== 0)
|
|
266
312
|
throw new Error("docker compose up --build a échoué");
|
|
267
313
|
}
|
|
268
|
-
// ─────────────────────────── backup ───────────────────────────
|
|
269
|
-
|
|
314
|
+
// ─────────────────────────── backup / restore ───────────────────────────
|
|
315
|
+
/** Sauvegardes de données (dossiers backups/<date>[-libellé]/ avec database.sql), plus récentes en dernier. */
|
|
316
|
+
function dataBackups(target) {
|
|
317
|
+
const root = path.join(target, "backups");
|
|
318
|
+
if (!fs.existsSync(root))
|
|
319
|
+
return [];
|
|
320
|
+
return fs
|
|
321
|
+
.readdirSync(root)
|
|
322
|
+
.filter((n) => fs.existsSync(path.join(root, n, "database.sql")))
|
|
323
|
+
.sort();
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Sauvegarde des données : base (database.sql, pg_dump) + images (storage/) dans backups/<date>[-libellé]/.
|
|
327
|
+
* --keep N : ne garde que les N sauvegardes les plus récentes (les copies de code « -code » ne sont jamais supprimées).
|
|
328
|
+
* Lancée aussi automatiquement avant `update` et avant `restore`.
|
|
329
|
+
*/
|
|
330
|
+
export function backup(dir, opts = {}) {
|
|
270
331
|
const target = path.resolve(dir);
|
|
271
|
-
readProject(target);
|
|
332
|
+
const p = readProject(target);
|
|
333
|
+
if (!isRunning(target, "postgres"))
|
|
334
|
+
throw new ProjectError("la base n'est pas démarrée : docker compose up -d, puis relancez la sauvegarde");
|
|
272
335
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
273
|
-
const
|
|
336
|
+
const name = opts.label ? `${stamp}-${opts.label}` : stamp;
|
|
337
|
+
const out = path.join(target, "backups", name);
|
|
274
338
|
fs.mkdirSync(out, { recursive: true });
|
|
275
339
|
const dump = composeOutput(target, ["exec", "-T", "postgres", "pg_dump", "-U", "medusa", "--clean", "--if-exists", "medusa"]);
|
|
276
340
|
if (dump.code !== 0)
|
|
@@ -278,8 +342,69 @@ export function backup(dir) {
|
|
|
278
342
|
fs.writeFileSync(path.join(out, "database.sql"), dump.stdout);
|
|
279
343
|
if (compose(target, ["cp", "storage:/data", path.join(out, "storage")]) !== 0)
|
|
280
344
|
throw new Error("copie des images échouée");
|
|
281
|
-
|
|
282
|
-
console.log(
|
|
345
|
+
fs.writeFileSync(path.join(out, "info.json"), JSON.stringify({ date: new Date().toISOString(), project: p.project, templateVersion: p.templateVersion, label: opts.label ?? null }, null, 2) + "\n");
|
|
346
|
+
console.log(pc.green(`✔ sauvegarde : backups/${name}/ (base + images)`));
|
|
347
|
+
if (opts.keep && opts.keep > 0) {
|
|
348
|
+
const all = dataBackups(target);
|
|
349
|
+
for (const old of all.slice(0, Math.max(0, all.length - opts.keep))) {
|
|
350
|
+
fs.rmSync(path.join(target, "backups", old), { recursive: true, force: true });
|
|
351
|
+
console.log(` ancienne sauvegarde supprimée : ${old}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return name;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Remet la base et les images d'une sauvegarde (la plus récente par défaut, ou --from <nom>).
|
|
358
|
+
* Fait d'abord une sauvegarde de l'état actuel (« avant-restauration ») : une restauration se défait toujours.
|
|
359
|
+
*/
|
|
360
|
+
export async function restore(dir, opts) {
|
|
361
|
+
const target = path.resolve(dir);
|
|
362
|
+
readProject(target);
|
|
363
|
+
const all = dataBackups(target);
|
|
364
|
+
if (!all.length)
|
|
365
|
+
throw new ProjectError("aucune sauvegarde dans backups/ (create-boutique backup .)");
|
|
366
|
+
const name = opts.from ? all.find((n) => n === opts.from || n.startsWith(opts.from)) : all[all.length - 1];
|
|
367
|
+
if (!name)
|
|
368
|
+
throw new ProjectError(`sauvegarde introuvable : ${opts.from}. Disponibles :\n ${all.join("\n ")}`);
|
|
369
|
+
const src = path.join(target, "backups", name);
|
|
370
|
+
console.log(`Sauvegarde à remettre : backups/${name}/`);
|
|
371
|
+
console.log(pc.yellow("Tout ce qui a été fait depuis (commandes, produits, campagnes, images) sera remplacé."));
|
|
372
|
+
if (!opts.yes) {
|
|
373
|
+
if (!process.stdin.isTTY)
|
|
374
|
+
throw new ProjectError("restauration : relancez avec --yes pour confirmer");
|
|
375
|
+
const ok = await prompts.confirm({ message: "Restaurer cette sauvegarde ?", initialValue: false });
|
|
376
|
+
if (prompts.isCancel(ok) || !ok)
|
|
377
|
+
throw new ProjectError("restauration annulée : rien n'a été modifié");
|
|
378
|
+
}
|
|
379
|
+
// 1. filet de sécurité : l'état actuel
|
|
380
|
+
backup(target, { label: "avant-restauration" });
|
|
381
|
+
// 2. plus aucune écriture pendant la restauration
|
|
382
|
+
compose(target, ["stop", "backend"]);
|
|
383
|
+
// 3. base
|
|
384
|
+
const sql = fs.readFileSync(path.join(src, "database.sql"), "utf8");
|
|
385
|
+
const r = composeOutput(target, ["exec", "-T", "postgres", "psql", "-q", "-v", "ON_ERROR_STOP=1", "-U", "medusa", "medusa"], sql);
|
|
386
|
+
if (r.code !== 0)
|
|
387
|
+
throw new Error(`restauration de la base échouée (le backend reste arrêté ; la sauvegarde « avant-restauration » permet de revenir) :\n${r.stderr.slice(-800)}`);
|
|
388
|
+
console.log(pc.green("✔ base restaurée"));
|
|
389
|
+
// 4. images : copie, puis propriétaire remis à l'utilisateur du stockage (sinon « File access denied »)
|
|
390
|
+
if (fs.existsSync(path.join(src, "storage"))) {
|
|
391
|
+
if (compose(target, ["cp", `backups/${name}/storage/.`, "storage:/data"]) !== 0)
|
|
392
|
+
throw new Error("restauration des images échouée");
|
|
393
|
+
const own = composeOutput(target, ["exec", "-T", "-u", "root", "storage", "sh", "-c", "chown -R \"$(stat -c %u:%g /data)\" /data"]);
|
|
394
|
+
if (own.code !== 0)
|
|
395
|
+
throw new Error(`droits des images non rétablis : ${own.stderr}`);
|
|
396
|
+
compose(target, ["restart", "storage"]);
|
|
397
|
+
console.log(pc.green("✔ images restaurées"));
|
|
398
|
+
}
|
|
399
|
+
// 5. relance
|
|
400
|
+
compose(target, ["up", "-d"]);
|
|
401
|
+
// Attendre que Docker déclare le backend prêt (migrations + démarrage : jusqu'à quelques minutes)
|
|
402
|
+
const healthy = () => composeOutput(target, ["ps", "backend", "--format", "{{.Status}}"]).stdout.includes("(healthy)");
|
|
403
|
+
for (let i = 0; i < 60 && !healthy(); i++)
|
|
404
|
+
await new Promise((res) => setTimeout(res, 5000));
|
|
405
|
+
if (!healthy())
|
|
406
|
+
console.log(pc.yellow("! le backend n'est pas encore prêt : docker compose logs -f backend"));
|
|
407
|
+
console.log(pc.green(`✔ restauration terminée (${name}). Pour revenir en arrière : create-boutique restore . --from ${dataBackups(target).filter((n) => n.endsWith("avant-restauration")).pop()}`));
|
|
283
408
|
}
|
|
284
409
|
// ─────────────────────────── campaign export / import ───────────────────────────
|
|
285
410
|
export function campaignExport(dir) {
|
|
@@ -341,7 +466,8 @@ export async function check(dir) {
|
|
|
341
466
|
const url = boutiqueUrl(p, b);
|
|
342
467
|
const html = await fetch(url).then((r) => (r.ok ? r.text() : Promise.reject(new Error(`boutique HTTP ${r.status}`))));
|
|
343
468
|
// Mode single : le site n'affiche que la boutique active
|
|
344
|
-
|
|
469
|
+
// Variante (theme.json → layout) : data-theme = mise en page empruntée, data-variant = le thème lui-même
|
|
470
|
+
if (p.mode !== "single" && !html.includes(`data-theme="${b.theme}"`) && !html.includes(`data-variant="${b.theme}"`))
|
|
345
471
|
throw new Error(`thème ${b.theme} absent de la page`);
|
|
346
472
|
steps.push("page");
|
|
347
473
|
const key = composeOutput(target, ["exec", "-T", "backend", "cat", `/app/keys/${b.slug}`]).stdout.trim();
|
|
@@ -392,8 +518,16 @@ export async function check(dir) {
|
|
|
392
518
|
console.log(pc.yellow("! Stripe non configuré : paiement manuel uniquement (STRIPE_API_KEY dans .env)"));
|
|
393
519
|
else if (!env.STRIPE_WEBHOOK_SECRET)
|
|
394
520
|
console.log(pc.yellow("! STRIPE_WEBHOOK_SECRET vide : webhook Stripe à créer (voir README)"));
|
|
395
|
-
if (!env.RESEND_API_KEY)
|
|
396
|
-
console.log(pc.yellow("!
|
|
521
|
+
if (!env.RESEND_API_KEY && !(env.SMTP_HOST && env.SMTP_USER && env.SMTP_PASS)) {
|
|
522
|
+
console.log(pc.yellow("! Ni Resend ni SMTP configuré : aucun e-mail n'est envoyé aux clients (voir README)"));
|
|
523
|
+
}
|
|
524
|
+
if (env.NABOOPAY_API_KEY && !env.NABOOPAY_WEBHOOK_SECRET) {
|
|
525
|
+
console.log(pc.yellow("! NABOOPAY_WEBHOOK_SECRET vide : les notifications NabooPay sont ignorées (voir README)"));
|
|
526
|
+
}
|
|
527
|
+
if (env.PAYTECH_API_KEY && env.PAYTECH_ENV !== "prod")
|
|
528
|
+
console.log(pc.yellow("! PayTech en mode test (PAYTECH_ENV=test)"));
|
|
529
|
+
if (env.PAYDUNYA_MASTER_KEY && env.PAYDUNYA_ENV !== "prod")
|
|
530
|
+
console.log(pc.yellow("! PayDunya en mode sandbox (PAYDUNYA_ENV=test)"));
|
|
397
531
|
console.log(pc.dim(" Les commandes de test (check@example.com) peuvent être annulées dans l'admin."));
|
|
398
532
|
if (failed)
|
|
399
533
|
throw new Error(`${failed} boutique(s) en échec`);
|
package/cli/dist/create.js
CHANGED
|
@@ -6,9 +6,11 @@ import { z } from "zod";
|
|
|
6
6
|
import { writeCampaignFiles, writeCompose, writeEnv, writeReadme } from "./generate.js";
|
|
7
7
|
import { writeGuides } from "./guide.js";
|
|
8
8
|
import { copyDir, copyTemplate, templateVersion } from "./paths.js";
|
|
9
|
+
import { syncCustomCss, writeFingerprint } from "./code.js";
|
|
9
10
|
import { nextFreePort, reservedByOthers, reservePorts } from "./ports.js";
|
|
10
|
-
import { normalizeUrl, storePorts, validateProject, writeJsonSchema, writeProject, ProjectError } from "./project.js";
|
|
11
|
+
import { boutiqueUrl, normalizeUrl, storePorts, validateProject, writeJsonSchema, writeProject, ProjectError } from "./project.js";
|
|
11
12
|
import { listThemes, themeIds } from "./themes.js";
|
|
13
|
+
import { askMail, askMobile, mobileSummary } from "./payments.js";
|
|
12
14
|
/** Réponses du CLI : mêmes clés pour les options, le fichier --config et les questions. */
|
|
13
15
|
export const answersSchema = z.object({
|
|
14
16
|
name: z.string().min(1).max(60).optional(),
|
|
@@ -30,6 +32,22 @@ export const answersSchema = z.object({
|
|
|
30
32
|
stripeWhsec: z.string().optional(),
|
|
31
33
|
resendKey: z.string().optional(),
|
|
32
34
|
emailFrom: z.email().optional(),
|
|
35
|
+
/** E-mails par SMTP (ex. Gmail + mot de passe d'application) si pas de Resend */
|
|
36
|
+
smtpHost: z.string().optional(),
|
|
37
|
+
smtpPort: z.number().int().optional(),
|
|
38
|
+
smtpUser: z.string().optional(),
|
|
39
|
+
smtpPass: z.string().optional(),
|
|
40
|
+
/** Paiement mobile (Wave, Orange Money…) : agrégateurs et leurs clés (voir cli/src/payments.ts) */
|
|
41
|
+
mobile: z.array(z.enum(["paytech", "paydunya", "naboopay"])).optional(),
|
|
42
|
+
paytechKey: z.string().optional(),
|
|
43
|
+
paytechSecret: z.string().optional(),
|
|
44
|
+
paytechEnv: z.enum(["test", "prod"]).optional(),
|
|
45
|
+
paydunyaMaster: z.string().optional(),
|
|
46
|
+
paydunyaPrivate: z.string().optional(),
|
|
47
|
+
paydunyaToken: z.string().optional(),
|
|
48
|
+
paydunyaEnv: z.enum(["test", "prod"]).optional(),
|
|
49
|
+
naboopayKey: z.string().optional(),
|
|
50
|
+
naboopaySecret: z.string().optional(),
|
|
33
51
|
products: z.string().optional(),
|
|
34
52
|
images: z.string().optional(),
|
|
35
53
|
storePort: z.number().int().optional(),
|
|
@@ -113,10 +131,14 @@ async function ask(a, dirName, yes) {
|
|
|
113
131
|
: a.filesUrl;
|
|
114
132
|
const stripePk = a.stripePk ?? (cancelled(await p.text({ message: "Clé Stripe publique (vide = paiement manuel)", placeholder: "pk_test_…", defaultValue: "" })) || undefined);
|
|
115
133
|
const stripeSk = stripePk ? (a.stripeSk ?? (cancelled(await p.password({ message: "Clé Stripe secrète" })) || undefined)) : a.stripeSk;
|
|
116
|
-
//
|
|
134
|
+
// Paiement mobile (Wave, Orange Money…) : critères de chaque agrégateur expliqués avant le choix
|
|
135
|
+
const mobile = await askMobile(a, [currency]);
|
|
136
|
+
// E-mails clients : Resend, SMTP (Gmail…) ou aucun. Déjà donnés en option / --config : pas de question.
|
|
137
|
+
const mail = a.resendKey || a.smtpHost ? { kind: a.resendKey ? "resend" : "smtp" } : await askMail();
|
|
117
138
|
const resendKey = a.resendKey ??
|
|
118
|
-
(cancelled(await p.password({ message: "Clé API Resend (vide = aucun e-mail envoyé)", validate: (v) => (!v || v.startsWith("re_") ? undefined : "une clé Resend commence par re_") })) ||
|
|
139
|
+
(mail.kind !== "resend" ? undefined : cancelled(await p.password({ message: "Clé API Resend (vide = aucun e-mail envoyé)", validate: (v) => (!v || v.startsWith("re_") ? undefined : "une clé Resend commence par re_") })) ||
|
|
119
140
|
undefined);
|
|
141
|
+
const smtp = "smtp" in mail ? mail.smtp : undefined;
|
|
120
142
|
const emailFrom = resendKey
|
|
121
143
|
? (a.emailFrom ??
|
|
122
144
|
cancelled(await p.text({
|
|
@@ -135,7 +157,7 @@ async function ask(a, dirName, yes) {
|
|
|
135
157
|
};
|
|
136
158
|
const products = a.products ?? (cancelled(await p.text({ message: "Fichier produits (CSV, optionnel)", defaultValue: "", validate: exists("file") })).trim() || undefined);
|
|
137
159
|
const images = a.images ?? (cancelled(await p.text({ message: "Dossier images (optionnel)", defaultValue: "", validate: exists("dir") })).trim() || undefined);
|
|
138
|
-
return { ...a, name, email, currency, country, language, mode, apiUrl, filesUrl, themes: chosen, boutiques: extra, stripePk, stripeSk, resendKey, emailFrom, products, images };
|
|
160
|
+
return { ...a, ...mobile, ...smtp, name, email, currency, country, language, mode, apiUrl, filesUrl, themes: chosen, boutiques: extra, stripePk, stripeSk, resendKey, emailFrom, products, images };
|
|
139
161
|
}
|
|
140
162
|
export async function create(dir, input, opts) {
|
|
141
163
|
const target = path.resolve(dir);
|
|
@@ -221,7 +243,8 @@ export async function create(dir, input, opts) {
|
|
|
221
243
|
`Images : ${proj.urls ? `${proj.urls.files}/medusa` : `http://localhost:${proj.storagePort}/medusa (local : à changer avant la production avec create-boutique urls)`}`,
|
|
222
244
|
`Devise ${a.currency.toUpperCase()} · pays ${a.country.toUpperCase()} · langue ${a.language}`,
|
|
223
245
|
`Paiement : ${a.stripeSk ? "Stripe + manuel" : "manuel (Stripe : non configuré)"}`,
|
|
224
|
-
`E-mails : ${a.resendKey ? `Resend, depuis ${a.emailFrom ?? "(adresse manquante)"}` : "
|
|
246
|
+
`E-mails : ${a.resendKey ? `Resend, depuis ${a.emailFrom ?? "(adresse manquante)"}` : a.smtpHost ? `SMTP ${a.smtpHost}${a.smtpUser ? `, depuis ${a.smtpUser}` : ""}${a.smtpPass ? "" : " (mot de passe à mettre dans .env)"}` : "désactivés (Resend ou SMTP à renseigner dans .env)"}`,
|
|
247
|
+
mobileSummary(a),
|
|
225
248
|
`Catalogue : ${a.products ?? "aucun (à ajouter dans l'admin)"}`,
|
|
226
249
|
];
|
|
227
250
|
if (opts.yes)
|
|
@@ -236,6 +259,8 @@ export async function create(dir, input, opts) {
|
|
|
236
259
|
// Écriture du projet
|
|
237
260
|
copyTemplate("storefront", target);
|
|
238
261
|
copyTemplate("backend", target);
|
|
262
|
+
syncCustomCss(target, proj.boutiques.map((b) => ({ slug: b.slug, theme: b.theme, url: boutiqueUrl(proj, b) })));
|
|
263
|
+
writeFingerprint(target, templateVersion()); // empreinte du code généré : update saura dire ce qui a été modifié à la main
|
|
239
264
|
writeProject(target, proj);
|
|
240
265
|
writeJsonSchema(target);
|
|
241
266
|
fs.mkdirSync(path.join(target, "seed", "images"), { recursive: true });
|
|
@@ -251,6 +276,19 @@ export async function create(dir, input, opts) {
|
|
|
251
276
|
stripeWhsec: a.stripeWhsec,
|
|
252
277
|
resendKey: a.resendKey,
|
|
253
278
|
emailFrom: a.emailFrom,
|
|
279
|
+
smtpHost: a.smtpHost,
|
|
280
|
+
smtpPort: a.smtpPort,
|
|
281
|
+
smtpUser: a.smtpUser,
|
|
282
|
+
smtpPass: a.smtpPass,
|
|
283
|
+
paytechKey: a.paytechKey,
|
|
284
|
+
paytechSecret: a.paytechSecret,
|
|
285
|
+
paytechEnv: a.paytechEnv,
|
|
286
|
+
paydunyaMaster: a.paydunyaMaster,
|
|
287
|
+
paydunyaPrivate: a.paydunyaPrivate,
|
|
288
|
+
paydunyaToken: a.paydunyaToken,
|
|
289
|
+
paydunyaEnv: a.paydunyaEnv,
|
|
290
|
+
naboopayKey: a.naboopayKey,
|
|
291
|
+
naboopaySecret: a.naboopaySecret,
|
|
254
292
|
});
|
|
255
293
|
writeCompose(target, proj);
|
|
256
294
|
writeReadme(target, proj);
|
|
@@ -261,8 +299,17 @@ export async function create(dir, input, opts) {
|
|
|
261
299
|
console.log(pc.yellow(`Chrome/Edge introuvable : ${path.basename(g.file)} à imprimer en PDF.`));
|
|
262
300
|
}
|
|
263
301
|
const next = `cd ${path.relative(process.cwd(), target) || "."}\ndocker compose up -d`;
|
|
302
|
+
// Personnaliser : ce qui est conservé par les mises à jour, et ce qui ne l'est pas
|
|
303
|
+
const custom = [
|
|
304
|
+
"Personnaliser l'apparence (conservé par create-boutique update) :",
|
|
305
|
+
' storefront/src/styles/custom.css : une section par boutique ([data-boutique="<slug>"]) + une commune',
|
|
306
|
+
" storefront/PERSONNALISER.md : mode d'emploi (plan du site page par page, classes, exemples)",
|
|
307
|
+
" storefront/public/custom/ : images et polices utilisées par custom.css",
|
|
308
|
+
" puis : docker compose up -d --build",
|
|
309
|
+
"Modifier d'autres fichiers du code reste possible, mais update les remplace (il les liste et les copie dans backups/ avant).",
|
|
310
|
+
].join("\n ");
|
|
264
311
|
if (opts.yes)
|
|
265
|
-
console.log(`Projet créé : ${target}\nEnsuite :\n ${next.replace("\n", "\n ")}\n`);
|
|
312
|
+
console.log(`Projet créé : ${target}\nEnsuite :\n ${next.replace("\n", "\n ")}\n\n ${custom}\n`);
|
|
266
313
|
else
|
|
267
|
-
p.outro(`Projet créé. Ensuite :\n\n ${pc.cyan(next.replace("\n", "\n "))}\n\n Mot de passe admin : ADMIN_PASSWORD dans .env (à transmettre à part)
|
|
314
|
+
p.outro(`Projet créé. Ensuite :\n\n ${pc.cyan(next.replace("\n", "\n "))}\n\n Mot de passe admin : ADMIN_PASSWORD dans .env (à transmettre à part).\n\n ${custom}`);
|
|
268
315
|
}
|
package/cli/dist/generate.js
CHANGED
|
@@ -185,9 +185,27 @@ export function writeEnv(dir, p, s) {
|
|
|
185
185
|
`STRIPE_WEBHOOK_SECRET=${s.stripeWhsec ?? ""}`,
|
|
186
186
|
`STRIPE_PUBLISHABLE_KEY=${s.stripePk ?? ""}`,
|
|
187
187
|
"",
|
|
188
|
-
"# E-mails
|
|
188
|
+
"# E-mails : Resend (prioritaire) OU un serveur SMTP ; les deux vides = aucun e-mail envoyé",
|
|
189
189
|
`RESEND_API_KEY=${s.resendKey ?? ""}`,
|
|
190
190
|
`EMAIL_FROM=${s.emailFrom ?? ""}`,
|
|
191
|
+
"# SMTP, ex. Gmail : SMTP_HOST=smtp.gmail.com, SMTP_PORT=465, SMTP_USER=adresse gmail, SMTP_PASS=mot de passe d'application",
|
|
192
|
+
`SMTP_HOST=${s.smtpHost ?? ""}`,
|
|
193
|
+
`SMTP_PORT=${s.smtpPort ?? 465}`,
|
|
194
|
+
`SMTP_USER=${s.smtpUser ?? ""}`,
|
|
195
|
+
`SMTP_PASS=${s.smtpPass ?? ""}`,
|
|
196
|
+
"",
|
|
197
|
+
"# Paiement Wave / Orange Money / carte (Afrique de l'Ouest, devise XOF). Vide = désactivé. Après modification :",
|
|
198
|
+
"# docker compose restart backend. Webhooks : voir README (section Paiement mobile).",
|
|
199
|
+
`PAYTECH_API_KEY=${s.paytechKey ?? ""}`,
|
|
200
|
+
`PAYTECH_API_SECRET=${s.paytechSecret ?? ""}`,
|
|
201
|
+
`PAYTECH_ENV=${s.paytechEnv ?? "test"}`,
|
|
202
|
+
`PAYDUNYA_MASTER_KEY=${s.paydunyaMaster ?? ""}`,
|
|
203
|
+
`PAYDUNYA_PRIVATE_KEY=${s.paydunyaPrivate ?? ""}`,
|
|
204
|
+
`PAYDUNYA_TOKEN=${s.paydunyaToken ?? ""}`,
|
|
205
|
+
`PAYDUNYA_ENV=${s.paydunyaEnv ?? "test"}`,
|
|
206
|
+
`NABOOPAY_API_KEY=${s.naboopayKey ?? ""}`,
|
|
207
|
+
`NABOOPAY_WEBHOOK_SECRET=${s.naboopaySecret ?? ""}`,
|
|
208
|
+
"NABOOPAY_METHODS=wave,orange_money",
|
|
191
209
|
"",
|
|
192
210
|
"# Dérivé de config/project.json (réécrit par `create-boutique apply`)",
|
|
193
211
|
...Object.entries(derivedEnv(p)).map(([k, v]) => `${k}=${v}`),
|
|
@@ -269,13 +287,28 @@ export function campaignDates(theme, now = new Date()) {
|
|
|
269
287
|
return [new Date(Date.UTC(y, 11, 1)), day(new Date(Date.UTC(y, 11, 24)), 0, true)];
|
|
270
288
|
case "nouvel-an":
|
|
271
289
|
return [new Date(Date.UTC(y, 11, 26)), day(new Date(Date.UTC(y + 1, 0, 2)), 0, true)];
|
|
290
|
+
// Saison d'été (solstice → fin août) ; Jeux et sport : l'été sportif (mi-juillet → fin août)
|
|
291
|
+
case "summer":
|
|
292
|
+
return [new Date(Date.UTC(y, 5, 21)), day(new Date(Date.UTC(y, 7, 31)), 0, true)];
|
|
293
|
+
case "jeux":
|
|
294
|
+
case "sport":
|
|
295
|
+
return [new Date(Date.UTC(y, 6, 15)), day(new Date(Date.UTC(y, 7, 31)), 0, true)];
|
|
296
|
+
// Ramadan → 2 jours après l'Aïd el-Fitr (Korité). Calendrier lunaire : dates approximatives (± 1 jour selon
|
|
297
|
+
// l'observation de la lune), à ajuster dans l'admin ; pour la Tabaski, le client change simplement les dates.
|
|
298
|
+
case "mubarak": {
|
|
299
|
+
const ramadan = {
|
|
300
|
+
2027: [1, 8, 2, 11], 2028: [0, 28, 2, 1], 2029: [0, 16, 1, 17], 2030: [0, 6, 1, 7],
|
|
301
|
+
};
|
|
302
|
+
const r = ramadan[y];
|
|
303
|
+
return r ? [new Date(Date.UTC(y, r[0], r[1])), day(new Date(Date.UTC(y, r[2], r[3])), 0, true)] : null;
|
|
304
|
+
}
|
|
272
305
|
default:
|
|
273
306
|
return null;
|
|
274
307
|
}
|
|
275
308
|
};
|
|
276
309
|
let range = build(now.getUTCFullYear());
|
|
277
|
-
if (range
|
|
278
|
-
range = build(now.getUTCFullYear() + 1);
|
|
310
|
+
if (!range || range[1] < now)
|
|
311
|
+
range = build(now.getUTCFullYear() + 1) ?? range;
|
|
279
312
|
if (!range)
|
|
280
313
|
range = [day(now), day(now, 30, true)];
|
|
281
314
|
return { startsAt: range[0].toISOString(), endsAt: range[1].toISOString() };
|
|
@@ -373,13 +406,41 @@ create-boutique backup . # sauvegarde base + images dans backups/<date>/
|
|
|
373
406
|
puis copier le secret \`whsec_…\` dans \`STRIPE_WEBHOOK_SECRET\` et \`docker compose restart backend\`.
|
|
374
407
|
4. Apple Pay : déclarer les domaines des boutiques dans Stripe → Paramètres → Moyens de paiement.
|
|
375
408
|
|
|
409
|
+
## Paiement mobile : Wave, Orange Money, carte (PayTech, PayDunya, NabooPay)
|
|
410
|
+
|
|
411
|
+
Pour les boutiques en **XOF** (FCFA ; PayTech accepte aussi EUR et USD). Le client choisit « Wave, Orange Money… »,
|
|
412
|
+
paie sur la page sécurisée de l'agrégateur, puis revient sur la boutique. Le montant est toujours calculé par le backend.
|
|
413
|
+
|
|
414
|
+
1. Créer un compte chez l'agrégateur choisi et récupérer ses clés (de test d'abord) :
|
|
415
|
+
- **PayTech** (paytech.sn) : \`PAYTECH_API_KEY\`, \`PAYTECH_API_SECRET\` ; \`PAYTECH_ENV=test\` (débite 100 à 150 FCFA) puis \`prod\`.
|
|
416
|
+
- **PayDunya** (paydunya.com) : \`PAYDUNYA_MASTER_KEY\`, \`PAYDUNYA_PRIVATE_KEY\`, \`PAYDUNYA_TOKEN\` ; \`PAYDUNYA_ENV=test\` (sandbox) puis \`prod\`.
|
|
417
|
+
- **NabooPay** (naboopay.com) : \`NABOOPAY_API_KEY\` ; \`NABOOPAY_METHODS=wave,orange_money\`.
|
|
418
|
+
2. Les écrire dans \`.env\` (jamais ailleurs), puis \`docker compose restart backend\` : le moyen apparaît au paiement.
|
|
419
|
+
3. Notifications (le paiement est confirmé même si le client ferme la page) : le backend doit être en ligne en HTTPS.
|
|
420
|
+
- PayTech et PayDunya : rien à faire, l'adresse est envoyée à chaque paiement
|
|
421
|
+
(\`<adresse du backend>/hooks/payment/paytech_paytech\`, \`…/paydunya_paydunya\`).
|
|
422
|
+
- NabooPay : tableau de bord → Paramètres → Intégration, webhook \`<adresse du backend>/hooks/payment/naboopay_naboopay\`,
|
|
423
|
+
puis copier son secret dans \`NABOOPAY_WEBHOOK_SECRET\` et \`docker compose restart backend\`.
|
|
424
|
+
4. Remboursement : PayTech et NabooPay depuis l'admin (remboursement total) ; PayDunya dans son tableau de bord, puis
|
|
425
|
+
« Remboursement » dans l'admin pour l'enregistrer (guide client, section 4.3).
|
|
426
|
+
5. Test PayTech (\`PAYTECH_ENV=test\`) : pas de simulation, chaque paiement débite vraiment 100 à 150 FCFA sur le compte
|
|
427
|
+
du testeur, reçus sur le solde PayTech de la boutique (moins les frais).
|
|
428
|
+
|
|
429
|
+
## E-mails sans Resend : SMTP (ex. Gmail)
|
|
430
|
+
|
|
431
|
+
Si \`RESEND_API_KEY\` est vide, les e-mails partent par le serveur SMTP du \`.env\`. Avec Gmail :
|
|
432
|
+
1. Compte Google → Sécurité → validation en deux étapes activée → **Mots de passe des applications** → en créer un.
|
|
433
|
+
2. \`.env\` : \`SMTP_HOST=smtp.gmail.com\`, \`SMTP_PORT=465\`, \`SMTP_USER=adresse@gmail.com\`, \`SMTP_PASS=<mot de passe d'application>\`,
|
|
434
|
+
\`EMAIL_FROM\` vide (l'adresse Gmail est utilisée), puis \`docker compose restart backend\`.
|
|
435
|
+
3. Limite Gmail : environ 500 e-mails par jour. Au-delà, ou pour un domaine à la marque : Resend, Brevo, Mailjet (SMTP aussi).
|
|
436
|
+
|
|
376
437
|
## Avant de livrer : checklist
|
|
377
438
|
|
|
378
439
|
- [ ] Chaque boutique s'ouvre à son adresse, avec son thème et sa devise.
|
|
379
440
|
- [ ] Chaque produit a un prix dans la devise de chaque boutique où il est vendu.
|
|
380
441
|
- [ ] Logo, textes et dates de chaque campagne vérifiés (Admin → Campagnes).
|
|
381
442
|
- [ ] Stripe configuré (clés + webhook) ou paiement manuel assumé.
|
|
382
|
-
- [ ] E-mails :
|
|
443
|
+
- [ ] E-mails : Resend ou SMTP renseigné, une commande test reçue par le client et le marchand.
|
|
383
444
|
- [ ] Une commande test passée dans chaque boutique (\`create-boutique check .\`).
|
|
384
445
|
- [ ] Sauvegarde programmée (\`create-boutique backup .\`).
|
|
385
446
|
|
package/cli/dist/guide.js
CHANGED
|
@@ -27,6 +27,14 @@ function render(source, out, dir, vars) {
|
|
|
27
27
|
let html = fs.readFileSync(source, "utf8");
|
|
28
28
|
for (const [k, v] of Object.entries(vars))
|
|
29
29
|
html = html.split(`{{${k}}}`).join(k.endsWith("_HTML") ? v : escapeHtml(v) || " ");
|
|
30
|
+
// Captures d'écran (docs/guide-images) intégrées au fichier : le PDF n'a besoin de rien à côté
|
|
31
|
+
html = html.replace(/src="guide-images\/([\w.-]+)"/g, (m, name) => {
|
|
32
|
+
const file = path.join(path.dirname(source), "guide-images", name);
|
|
33
|
+
if (!fs.existsSync(file))
|
|
34
|
+
return m;
|
|
35
|
+
const type = name.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
36
|
+
return `src="data:${type};base64,${fs.readFileSync(file).toString("base64")}"`;
|
|
37
|
+
});
|
|
30
38
|
const htmlFile = path.join(dir, `${out}.html`);
|
|
31
39
|
const pdfFile = path.join(dir, `${out}.pdf`);
|
|
32
40
|
fs.writeFileSync(htmlFile, html);
|
|
@@ -48,7 +56,7 @@ function render(source, out, dir, vars) {
|
|
|
48
56
|
*/
|
|
49
57
|
export function writeGuides(dir, p, adminEmail, support = "") {
|
|
50
58
|
const fr = p.defaults.language === "fr";
|
|
51
|
-
const adminUrl = `http://localhost:${p.apiPort}/app`;
|
|
59
|
+
const adminUrl = `${p.urls?.api ?? `http://localhost:${p.apiPort}`}/app`;
|
|
52
60
|
const stores = p.boutiques
|
|
53
61
|
.map((b) => {
|
|
54
62
|
const e = effective(p, b);
|
|
@@ -58,7 +66,7 @@ export function writeGuides(dir, p, adminEmail, support = "") {
|
|
|
58
66
|
const vars = {
|
|
59
67
|
ADMIN_URL: adminUrl,
|
|
60
68
|
ADMIN_EMAIL: adminEmail,
|
|
61
|
-
SUPPORT: support,
|
|
69
|
+
SUPPORT: support || "l'équipe qui a créé votre boutique",
|
|
62
70
|
PROJECT: p.project,
|
|
63
71
|
STORE_NAME: p.defaults.storeName,
|
|
64
72
|
CURRENCIES: currencies(p).map((c) => c.toUpperCase()).join(", "),
|