|
|
|
@@ -0,0 +1,291 @@
|
|
|
|
|
/* =====================================================================
|
|
|
|
|
test-kette.mjs — die GANZE Kette in einer Sitzung
|
|
|
|
|
|
|
|
|
|
Wunsch: "ich will dass du es abcheckst" — nicht Stück für Stück
|
|
|
|
|
(das ist schon geprüft: test-annahme, test-angebote, test-automatik,
|
|
|
|
|
test-suche, test-termine, zusammen 258 Prüfungen), sondern EINMAL
|
|
|
|
|
DURCHGÄNGIG, so wie ein echter Auftrag tatsächlich abläuft:
|
|
|
|
|
|
|
|
|
|
Anfrage kommt rein
|
|
|
|
|
→ ich schicke ein Angebot
|
|
|
|
|
→ der Kunde sieht es in seinem Portal
|
|
|
|
|
→ der Kunde sagt zu (Projekt + Anzahlungsrechnung entstehen)
|
|
|
|
|
→ die Anzahlung geht ein
|
|
|
|
|
→ die Uhr startet, der Termin wird neu gerechnet
|
|
|
|
|
→ ich bekomme eine Benachrichtigung
|
|
|
|
|
→ ich arbeite ein paar Punkte ab
|
|
|
|
|
→ der Kunde zahlt den Rest
|
|
|
|
|
→ ich breche NICHT ab (Gegenprobe: ein zweites Projekt, das ich
|
|
|
|
|
abbreche, MUSS aus der Liste verschwinden)
|
|
|
|
|
|
|
|
|
|
Wichtig: Läuft gegen die ECHTEN Funktionen aus server-internal/, nicht
|
|
|
|
|
gegen Nachbauten. Die Datenbank ist eine frische Wegwerf-Datei, damit
|
|
|
|
|
nichts von Filipes echten Daten berührt wird.
|
|
|
|
|
===================================================================== */
|
|
|
|
|
import { mkdtempSync, rmSync, writeSync } from "node:fs";
|
|
|
|
|
import { tmpdir } from "node:os";
|
|
|
|
|
import { join } from "node:path";
|
|
|
|
|
import { randomBytes, createHmac } from "node:crypto";
|
|
|
|
|
|
|
|
|
|
const L = (...a) => writeSync(1, a.join(" ") + "\n");
|
|
|
|
|
process.on("uncaughtException", (e) => {
|
|
|
|
|
writeSync(2, "\nABGEBROCHEN: " + ((e && e.stack) || e) + "\n"); process.exit(1);
|
|
|
|
|
});
|
|
|
|
|
process.on("unhandledRejection", (e) => {
|
|
|
|
|
writeSync(2, "\nABGELEHNT: " + ((e && e.stack) || e) + "\n"); process.exit(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const tmp = mkdtempSync(join(tmpdir(), "wd-kette-"));
|
|
|
|
|
process.env.DB_PATH = join(tmp, "t.db");
|
|
|
|
|
process.env.ENCRYPTION_KEY = randomBytes(32).toString("base64");
|
|
|
|
|
process.env.WEBDESIGN_API_SECRET = randomBytes(24).toString("hex");
|
|
|
|
|
process.env.WD_BASIS_URL = "https://dogfather-universe.com";
|
|
|
|
|
|
|
|
|
|
const { initDb, db } = await import("./db.js");
|
|
|
|
|
initDb();
|
|
|
|
|
const geheim = await import("./lib/webdesign-geheimnisse.js");
|
|
|
|
|
await geheim.geheimnisseLaden();
|
|
|
|
|
const { vorlagenSicherstellen } = await import("./lib/webdesign-vorlagen.js");
|
|
|
|
|
vorlagenSicherstellen();
|
|
|
|
|
|
|
|
|
|
const A = await import("./routes/webdesign-annahme.js");
|
|
|
|
|
const G = await import("./routes/webdesign-angebote.js");
|
|
|
|
|
const Z = await import("./routes/webdesign-zahlungen.js");
|
|
|
|
|
const Auto = await import("./lib/webdesign-automatik.js");
|
|
|
|
|
const P = await import("./routes/webdesign-portal.js");
|
|
|
|
|
const K = await import("./routes/webdesign-kunden.js");
|
|
|
|
|
const { tokenHash } = await import("./lib/webdesign-helfer.js");
|
|
|
|
|
const { werktageBis } = await import("./lib/webdesign-termine.js");
|
|
|
|
|
|
|
|
|
|
let ok = 0, fehler = 0;
|
|
|
|
|
const pruefe = (n, b, z) => b
|
|
|
|
|
? (ok++, L(" ok " + n))
|
|
|
|
|
: (fehler++, L(" FEHL " + n + (z !== undefined ? " -> " + z : "")));
|
|
|
|
|
|
|
|
|
|
function ausweis(g = 60000) {
|
|
|
|
|
const n = Buffer.from(JSON.stringify({ bereich: "wd-admin", rolle: "dogfather", exp: Date.now() + g }))
|
|
|
|
|
.toString("base64url");
|
|
|
|
|
return n + "." + createHmac("sha256", process.env.WEBDESIGN_API_SECRET).update(n).digest("base64url");
|
|
|
|
|
}
|
|
|
|
|
async function admin(fn, extra) {
|
|
|
|
|
let out = null;
|
|
|
|
|
await fn({ headers: { authorization: "Bearer " + ausweis() }, cookies: {},
|
|
|
|
|
params: {}, body: {}, query: {}, ...extra },
|
|
|
|
|
{ status() { return this; }, json(o) { out = o; return this; }, setHeader() {}, end() {} });
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
/* Eine ECHTE Kundensitzung anlegen, damit portalUebersicht ueber den
|
|
|
|
|
normalen Code-Pfad laeuft -- nicht nachgebaut. */
|
|
|
|
|
function kundenToken(kundeId) {
|
|
|
|
|
const roh = randomBytes(24).toString("hex");
|
|
|
|
|
db.prepare(
|
|
|
|
|
`INSERT INTO wd_kunden_sitzungen (token_hash, kunde_id, erstellt_am, laeuft_ab)
|
|
|
|
|
VALUES (?,?,?,?)`
|
|
|
|
|
).run(tokenHash(roh), kundeId, new Date().toISOString(),
|
|
|
|
|
new Date(Date.now() + 3600000).toISOString());
|
|
|
|
|
return roh;
|
|
|
|
|
}
|
|
|
|
|
async function portalAls(kundeId) {
|
|
|
|
|
let out = null;
|
|
|
|
|
await P.portalUebersicht(
|
|
|
|
|
{ headers: { authorization: "Bearer " + kundenToken(kundeId) }, cookies: {} },
|
|
|
|
|
{ status() { return this; }, json(o) { out = o; return this; }, setHeader() {}, end() {} }
|
|
|
|
|
);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function kunde(fn, kundeObj, extra, ...rest) {
|
|
|
|
|
let out = null;
|
|
|
|
|
await fn({ headers: {}, cookies: {}, params: {}, body: {}, query: {}, ...extra },
|
|
|
|
|
{ status() { return this; }, json(o) { out = o; return this; }, setHeader() {}, end() {} },
|
|
|
|
|
kundeObj, ...rest);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
L("SCHRITT 1 — eine Anfrage kommt rein (wie über das echte Formular)");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
const jetztIso = new Date().toISOString();
|
|
|
|
|
db.prepare(
|
|
|
|
|
`INSERT INTO wd_anfragen (id,nummer,name,email,telefon,paket,ziel,details,status,sprache,erstellt_am)
|
|
|
|
|
VALUES ('a1','A-2608-0099','Vanessa Geske','[email protected]','+352 691 000000',
|
|
|
|
|
'onepager','Mehr Sichtbarkeit','Bitte schlicht und modern.','neu','de',?)`
|
|
|
|
|
).run(jetztIso);
|
|
|
|
|
pruefe("die Anfrage steht in der Datenbank",
|
|
|
|
|
db.prepare(`SELECT status FROM wd_anfragen WHERE id='a1'`).get().status === "neu");
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L("SCHRITT 2 — ich sehe mir die Angebotsvorschau an (nichts eingetippt)");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
const vorschau = await admin(G.angebotVorschau, { params: { id: "a1" } });
|
|
|
|
|
pruefe("die Vorschau liefert einen Preisvorschlag", vorschau.preisEuro > 0, vorschau.preisEuro);
|
|
|
|
|
pruefe("und einen fertigen Leistungstext aus der Standardliste",
|
|
|
|
|
(vorschau.leistungen || "").length > 50, vorschau.leistungen.length + " Zeichen");
|
|
|
|
|
pruefe("noch ist NICHTS angelegt",
|
|
|
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_angebote`).get().n === 0);
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L("SCHRITT 3 — ich schicke das Angebot raus");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
const gesendet = await admin(G.angebotSenden, { params: { id: "a1" },
|
|
|
|
|
body: { paket: "onepager", preisEuro: 490 } });
|
|
|
|
|
pruefe("das Angebot ist raus", gesendet.ok === true, JSON.stringify(gesendet).slice(0, 100));
|
|
|
|
|
pruefe("mit einer Nummer", /^AG-/.test(gesendet.nummer), gesendet.nummer);
|
|
|
|
|
pruefe("die Anfrage erkennt das automatisch (Status 'angebot')",
|
|
|
|
|
db.prepare(`SELECT status FROM wd_anfragen WHERE id='a1'`).get().status === "angebot");
|
|
|
|
|
const kundeId = db.prepare(`SELECT kunde_id FROM wd_anfragen WHERE id='a1'`).get().kunde_id;
|
|
|
|
|
pruefe("ein Kundenzugang ist entstanden", !!kundeId);
|
|
|
|
|
/* Erst fuer falsch gehalten, dann verstanden: freigeschaltet steuert
|
|
|
|
|
das Portal-Login selbst. Waere es 0, koennte Vanessa sich gar nicht
|
|
|
|
|
einloggen, um ihr eigenes Angebot anzusehen -- es MUSS also sofort
|
|
|
|
|
auf 1 stehen, sonst ist das Angebot fuer sie unerreichbar. */
|
|
|
|
|
pruefe("er ist SOFORT freigeschaltet -- sonst koennte er sein eigenes Angebot nie ansehen",
|
|
|
|
|
db.prepare(`SELECT freigeschaltet FROM wd_kunden WHERE id=?`).get(kundeId).freigeschaltet === 1);
|
|
|
|
|
pruefe("das Angebot liegt im Postfach des Kunden",
|
|
|
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_postfach WHERE kunde_id=?`).get(kundeId).n === 1);
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L("SCHRITT 4 — Vanessa öffnet ihr Portal, das Angebot ist da");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
const kundeObj = { id: kundeId, name: "Vanessa Geske" };
|
|
|
|
|
const angebote = G.portalAngebote(kundeId);
|
|
|
|
|
pruefe("genau ein Angebot ist sichtbar", angebote.length === 1, angebote.length);
|
|
|
|
|
pruefe("mit lesbarem Preis", angebote[0].preis_text === "490,00 €", angebote[0].preis_text);
|
|
|
|
|
pruefe("annehmbar (nicht abgelaufen)", angebote[0].annehmbar === true);
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L("SCHRITT 5 — Vanessa sagt zu (der folgenreichste Klick der Seite)");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
const zusage = await kunde(G.portalAngebotZusagen, kundeObj,
|
|
|
|
|
{ params: { id: gesendet.id } }, "gehashter-ip-abdruck");
|
|
|
|
|
pruefe("die Zusage gelingt", zusage.ok === true, JSON.stringify(zusage).slice(0, 120));
|
|
|
|
|
const projektId = zusage.projektId;
|
|
|
|
|
const projektNummer = zusage.projektNummer;
|
|
|
|
|
pruefe("es gibt eine Projektnummer", /^P-/.test(projektNummer), projektNummer);
|
|
|
|
|
|
|
|
|
|
const projekt1 = db.prepare(`SELECT * FROM wd_projekte WHERE id=?`).get(projektId);
|
|
|
|
|
pruefe("das Projekt übernimmt den Preis aus dem Angebot",
|
|
|
|
|
projekt1.preis_cent === 49000, projekt1.preis_cent);
|
|
|
|
|
pruefe("die Aufgabenliste steht bereits",
|
|
|
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_aufgaben WHERE projekt_id=?`).get(projektId).n > 0);
|
|
|
|
|
pruefe("eine offene Anzahlungsrechnung ist entstanden",
|
|
|
|
|
!!db.prepare(`SELECT id FROM wd_zahlungen WHERE projekt_id=? AND art='anzahlung' AND status='offen'`).get(projektId));
|
|
|
|
|
pruefe("DIE UHR LÄUFT NOCH NICHT — genau die Regel, um die es ging",
|
|
|
|
|
projekt1.uhr_start_am === null, String(projekt1.uhr_start_am));
|
|
|
|
|
pruefe("Vanessa ist jetzt freigeschaltet",
|
|
|
|
|
db.prepare(`SELECT freigeschaltet FROM wd_kunden WHERE id=?`).get(kundeId).freigeschaltet === 1);
|
|
|
|
|
|
|
|
|
|
/* GEFUNDEN beim ersten Durchlauf dieser Kette: Vor der Zahlung zeigte
|
|
|
|
|
das Portal einen tickenden Countdown, obwohl noch nichts begonnen
|
|
|
|
|
hatte -- und der Termin haette sich beim Zahlungseingang sichtbar
|
|
|
|
|
verschoben. Reparatur in webdesign-portal.js: wartet_auf_anzahlung
|
|
|
|
|
ersetzt den Countdown, solange die Uhr steht. */
|
|
|
|
|
const portalVorZahlung = await portalAls(kundeId);
|
|
|
|
|
const projektImPortalVorZahlung = portalVorZahlung.projekte.find((p) => p.id === projektId);
|
|
|
|
|
pruefe("VOR der Zahlung: kein Countdown, sondern der Wartehinweis",
|
|
|
|
|
projektImPortalVorZahlung.wartet_auf_anzahlung === true
|
|
|
|
|
&& projektImPortalVorZahlung.verbleibende_werktage === null,
|
|
|
|
|
JSON.stringify({ wartet: projektImPortalVorZahlung.wartet_auf_anzahlung,
|
|
|
|
|
rest: projektImPortalVorZahlung.verbleibende_werktage }));
|
|
|
|
|
pruefe("aber der voraussichtliche Termin ist trotzdem sichtbar (kein Verstecken)",
|
|
|
|
|
!!projektImPortalVorZahlung.termin_am, projektImPortalVorZahlung.termin_am);
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L("SCHRITT 6 — die Anzahlung geht ein (wie von PayPal gemeldet)");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
const zahlung = db.prepare(`SELECT id FROM wd_zahlungen WHERE projekt_id=? AND art='anzahlung'`).get(projektId);
|
|
|
|
|
Auto.zahlungVerbucht(zahlung.id, "system"); // dieselbe Funktion, die der Webhook aufruft
|
|
|
|
|
|
|
|
|
|
const projekt2 = db.prepare(`SELECT * FROM wd_projekte WHERE id=?`).get(projektId);
|
|
|
|
|
pruefe("JETZT läuft die Uhr", !!projekt2.uhr_start_am, projekt2.uhr_start_am);
|
|
|
|
|
pruefe("der Termin ist ab HEUTE gerechnet (10 Werktage für Onepager)",
|
|
|
|
|
werktageBis(projekt2.termin_am, new Date()) === 10,
|
|
|
|
|
"noch " + werktageBis(projekt2.termin_am, new Date()) + " Werktage");
|
|
|
|
|
pruefe("der Status ist von briefing auf design gewechselt", projekt2.status === "design", projekt2.status);
|
|
|
|
|
pruefe("jetzt bin ich am Zug, nicht Vanessa", projekt2.wartet_auf === "dogfather", projekt2.wartet_auf);
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L("SCHRITT 7 — ich bekomme eine Benachrichtigung im Cockpit");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
const meldungen = Auto.meldungenLesen(true, 20);
|
|
|
|
|
const anzMeldung = meldungen.find((m) => m.art === "anzahlung_da" && m.ziel_id === projektId);
|
|
|
|
|
pruefe("die Meldung ist da", !!anzMeldung, JSON.stringify(meldungen.map((m) => m.art)));
|
|
|
|
|
pruefe("sie ist als dringend markiert", anzMeldung.dringend === 1);
|
|
|
|
|
pruefe("sie nennt Vanessa und den Betrag", /Vanessa/.test(anzMeldung.text) && /147,00/.test(anzMeldung.text),
|
|
|
|
|
anzMeldung.text);
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L("SCHRITT 8 — die Übersicht im Cockpit zeigt alles konsistent");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
const uebersichtsAntwort = await admin(Z.uebersicht, {});
|
|
|
|
|
pruefe("die Übersicht antwortet", uebersichtsAntwort.ok === true);
|
|
|
|
|
pruefe("das Projekt steht bei 'wartet auf Kunde' NICHT mehr drin (ich bin dran)",
|
|
|
|
|
!uebersichtsAntwort.projekte.some((p) => p.id === projektId && p.wartet_auf === "kunde") ,
|
|
|
|
|
JSON.stringify(uebersichtsAntwort.projekte.map((p) => ({ id: p.id, wartet_auf: p.wartet_auf }))));
|
|
|
|
|
pruefe("die ungelesene Meldung erscheint im Cockpit",
|
|
|
|
|
uebersichtsAntwort.meldungen.some((m) => m.id === anzMeldung.id));
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L("SCHRITT 9 — ich arbeite Punkte ab, der Fortschritt zieht mit");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
const aufgaben = db.prepare(`SELECT id FROM wd_aufgaben WHERE projekt_id=? ORDER BY reihenfolge`).all(projektId);
|
|
|
|
|
const halbe = Math.floor(aufgaben.length / 2);
|
|
|
|
|
for (let i = 0; i < halbe; i++) {
|
|
|
|
|
db.prepare(`UPDATE wd_aufgaben SET status='erledigt' WHERE id=?`).run(aufgaben[i].id);
|
|
|
|
|
}
|
|
|
|
|
// Fortschritt direkt aus der Datenbank nachrechnen (dieselbe Formel wie im Cockpit)
|
|
|
|
|
const fertig = db.prepare(`SELECT COUNT(*) AS n FROM wd_aufgaben WHERE projekt_id=? AND status='erledigt'`).get(projektId).n;
|
|
|
|
|
pruefe("die Hälfte ist erledigt", fertig === halbe, fertig + "/" + aufgaben.length);
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L("SCHRITT 10 — Vanessa zahlt den Rest");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
db.prepare(
|
|
|
|
|
`INSERT INTO wd_zahlungen (id,nummer,kunde_id,projekt_id,art,zweck_text,betrag_cent,waehrung,status,erstellt_am)
|
|
|
|
|
VALUES ('zrest','Z-2608-0099',?,?,'rest','Restzahlung',34300,'EUR','offen',?)`
|
|
|
|
|
).run(kundeId, projektId, jetztIso);
|
|
|
|
|
Auto.zahlungVerbucht("zrest", "system");
|
|
|
|
|
pruefe("die Restzahlung ist am Projekt vermerkt",
|
|
|
|
|
db.prepare(`SELECT rest_bezahlt FROM wd_projekte WHERE id=?`).get(projektId).rest_bezahlt === 1);
|
|
|
|
|
/* Der Betrag steht im TITEL der Meldung, der Name im TEXT -- beide
|
|
|
|
|
Felder zusammen pruefen, nicht nur eines. */
|
|
|
|
|
pruefe("es kam eine weitere Meldung mit Betrag und Namen",
|
|
|
|
|
Auto.meldungenLesen(true, 20).some((m) => /343,00/.test(m.titel) && /Vanessa/.test(m.text)),
|
|
|
|
|
JSON.stringify(Auto.meldungenLesen(true, 20).map((m) => m.titel)));
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L("SCHRITT 11 — GEGENPROBE: ein zweiter, abgebrochener Auftrag");
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
db.prepare(
|
|
|
|
|
`INSERT INTO wd_anfragen (id,nummer,name,email,paket,status,sprache,erstellt_am)
|
|
|
|
|
VALUES ('a2','A-2608-0100','Testkunde Zwei','[email protected]','website','neu','de',?)`
|
|
|
|
|
).run(jetztIso);
|
|
|
|
|
const angebot2 = await admin(G.angebotSenden, { params: { id: "a2" },
|
|
|
|
|
body: { paket: "website", preisEuro: 1490 } });
|
|
|
|
|
const kundeId2 = db.prepare(`SELECT kunde_id FROM wd_anfragen WHERE id='a2'`).get().kunde_id;
|
|
|
|
|
const zusage2 = await kunde(G.portalAngebotZusagen, { id: kundeId2, name: "Testkunde Zwei" },
|
|
|
|
|
{ params: { id: angebot2.id } }, "x");
|
|
|
|
|
const projektId2 = zusage2.projektId;
|
|
|
|
|
|
|
|
|
|
const listeVorAbbruch = await admin(K.projekteListe, {});
|
|
|
|
|
pruefe("das zweite Projekt steht in der laufenden Liste",
|
|
|
|
|
listeVorAbbruch.projekte.some((p) => p.id === projektId2));
|
|
|
|
|
|
|
|
|
|
const abbruch = await admin(A.projektAbbrechen, { params: { id: projektId2 },
|
|
|
|
|
body: { grund: "keine_zahlung", wer: "kunde" } });
|
|
|
|
|
pruefe("der Abbruch gelingt", abbruch.ok === true, JSON.stringify(abbruch).slice(0, 100));
|
|
|
|
|
|
|
|
|
|
const listeNachAbbruch = await admin(K.projekteListe, {});
|
|
|
|
|
pruefe("das abgebrochene Projekt ist aus der laufenden Liste WEG",
|
|
|
|
|
!listeNachAbbruch.projekte.some((p) => p.id === projektId2),
|
|
|
|
|
JSON.stringify(listeNachAbbruch.projekte.map((p) => p.id)));
|
|
|
|
|
pruefe("das ERSTE Projekt (Vanessa, läuft normal) steht weiter da",
|
|
|
|
|
listeNachAbbruch.projekte.some((p) => p.id === projektId));
|
|
|
|
|
pruefe("archiviert, nicht gelöscht — der Beleg bleibt",
|
|
|
|
|
!!db.prepare(`SELECT id FROM wd_projekte WHERE id=? AND archiviert=1`).get(projektId2));
|
|
|
|
|
|
|
|
|
|
L("\n========================================================================");
|
|
|
|
|
L(`ENDE DER KETTE: ${ok} bestanden, ${fehler} fehlgeschlagen`);
|
|
|
|
|
L("========================================================================");
|
|
|
|
|
process.exitCode = fehler ? 1 : 0;
|
|
|
|
|
rmSync(tmp, { recursive: true, force: true });
|