mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-11 17:27:54 +02:00
Deux comportements demandés : 1. Une promotion n'apparaît sur le site que pendant sa période de validité. Les lectures publiques passent par `VISIBLE_PROMO` (`src/lib/queries.ts`) : statut `live` **et** fenêtre `starts_on` / `ends_on`, journée calculée en Europe/Paris pour qu'un conteneur en UTC ne retire pas une offre deux heures trop tôt. Le statut n'est pas touché : hors période, le backoffice explique pourquoi l'offre n'est pas visible (`visibilityNote`) et elle revient d'elle-même si les dates changent. 2. La publication peut être programmée. `promotions.publish_at` : l'adhérent propose une date, le modérateur la garde, la déplace ou la vide dans le formulaire « Valider ». Avec une échéance future la promotion passe au statut `scheduled` — rien n'est visible, rien n'est diffusé — et `releaseDuePromotions()` fait la bascule à l'heure dite, site et réseaux ensemble. Un bouton « Publier maintenant » court-circuite l'attente. Mécanique : - `src/lib/promo-schedule.ts` (pur, testé) : conversion `datetime-local` ⇄ heure de l'association, changements d'heure compris, et refus d'une saisie illisible plutôt qu'une publication immédiate involontaire. - `src/lib/promo-publish.ts` reprend `publishPromoShares()` : deux appelants désormais, une action serveur et la boucle de libération, qui n'a pas de requête et ne peut donc pas appeler `revalidatePath()`. Idem pour `logActivity()` dans `src/lib/activity-log.ts`. - La bascule `scheduled → live` est un `UPDATE … RETURNING` filtré sur le statut : atomique, donc pas de double publication même à plusieurs instances, en plus de la garde existante sur `social_posts`. - Déclencheur : `src/instrumentation.ts`, boucle d'une minute avec rattrapage au démarrage (`PROMO_SCHEDULER=off` la désactive). Le travail vit dans `instrumentation-node.ts`, écarté du bundle edge par `next.config.mjs` : Next compile aussi l'instrumentation pour le middleware, où `pg` ne peut pas être embarqué. Migration 0016 : `promo_status += 'scheduled'`, `promotions.publish_at`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
68 lines
2.5 KiB
TypeScript
68 lines
2.5 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
formatSchedule,
|
|
isDue,
|
|
isPending,
|
|
parseScheduleInput,
|
|
scheduleLabel,
|
|
toScheduleInput,
|
|
} from "../src/lib/promo-schedule";
|
|
|
|
test("programmation — champ vide = publication immédiate", () => {
|
|
assert.equal(parseScheduleInput(""), null);
|
|
assert.equal(parseScheduleInput(null), null);
|
|
assert.equal(parseScheduleInput(" "), null);
|
|
assert.equal(isDue(null), true);
|
|
});
|
|
|
|
test("programmation — heure d'hiver interprétée en heure de Paris (UTC+1)", () => {
|
|
const d = parseScheduleInput("2027-01-15T09:00");
|
|
assert.ok(d);
|
|
assert.equal(d.toISOString(), "2027-01-15T08:00:00.000Z");
|
|
});
|
|
|
|
test("programmation — heure d'été interprétée en heure de Paris (UTC+2)", () => {
|
|
const d = parseScheduleInput("2027-07-15T09:00");
|
|
assert.ok(d);
|
|
assert.equal(d.toISOString(), "2027-07-15T07:00:00.000Z");
|
|
});
|
|
|
|
test("programmation — minuit ne bascule pas d'un jour", () => {
|
|
const d = parseScheduleInput("2027-03-10T00:00");
|
|
assert.ok(d);
|
|
assert.equal(d.toISOString(), "2027-03-09T23:00:00.000Z");
|
|
});
|
|
|
|
test("programmation — aller-retour avec le champ du formulaire", () => {
|
|
for (const value of ["2027-01-15T09:00", "2027-07-15T18:30", "2027-12-31T23:45"]) {
|
|
assert.equal(toScheduleInput(parseScheduleInput(value)), value);
|
|
}
|
|
assert.equal(toScheduleInput(null), "");
|
|
});
|
|
|
|
test("programmation — une saisie illisible est refusée, jamais ignorée", () => {
|
|
assert.throws(() => parseScheduleInput("demain matin"));
|
|
assert.throws(() => parseScheduleInput("2027-13-45T99:99"));
|
|
assert.throws(() => parseScheduleInput("2027-01-15"));
|
|
});
|
|
|
|
test("programmation — échéance atteinte", () => {
|
|
const at = new Date("2027-01-15T08:00:00.000Z");
|
|
assert.equal(isDue(at, new Date("2027-01-15T07:59:59.000Z")), false);
|
|
assert.equal(isDue(at, at), true);
|
|
assert.equal(isDue(at, new Date("2027-01-15T08:00:01.000Z")), true);
|
|
assert.equal(isPending(at, new Date("2027-01-15T07:00:00.000Z")), true);
|
|
});
|
|
|
|
test("programmation — libellé en heure de Paris", () => {
|
|
assert.equal(formatSchedule(new Date("2027-01-15T08:00:00.000Z")), "le 15 janvier 2027 à 09:00");
|
|
assert.equal(formatSchedule(new Date("2027-07-15T07:00:00.000Z")), "le 15 juillet 2027 à 09:00");
|
|
assert.equal(formatSchedule(null), null);
|
|
assert.equal(
|
|
scheduleLabel(new Date("2027-01-15T08:00:00.000Z")),
|
|
"Publication programmée le 15 janvier 2027 à 09:00"
|
|
);
|
|
assert.equal(scheduleLabel(null), null);
|
|
});
|