@mostajs/aop-alerts 0.1.0
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/README.md +5 -0
- package/llms.txt +7 -0
- package/package.json +23 -0
- package/src/aop-alerts.js +17 -0
- package/src/index.js +4 -0
- package/src/match.js +34 -0
- package/src/memory-repo.js +2 -0
- package/src/schemas.js +15 -0
package/README.md
ADDED
package/llms.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @mostajs/aop-alerts — fiche LLM
|
|
2
|
+
> Alertes de veille AO (ORM-first) : critères, matching pondéré PUR (seuil 30%), planification.
|
|
3
|
+
|
|
4
|
+
## EXPORTS
|
|
5
|
+
AlertSchema · matchAlert(alert,tender)->{matches,score} · nextRun(alert,now) · createAlerts({repositories:{alerts}}) · createMemoryRepositories()
|
|
6
|
+
Poids: keywords 30, cpv 20, budget 15, types 10, procedures 10, location 15. excludeKeywords => rejet. Seuil match = 30%.
|
|
7
|
+
Stack mosta-aop-stack. run(alert,tenders) trie par score.
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/aop-alerts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Alertes de veille d'appels d'offres (ORM-first) : critères, matching pondéré pur (seuil 30%), planification. Extrait d'AgoraScope.",
|
|
5
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
|
+
"license": "AGPL-3.0-or-later",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"llms.txt"
|
|
16
|
+
],
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@mostajs/mjs-unit": "^0.3.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node test-scripts/unit/aop-alerts.test.mjs && node examples/run.mjs"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @mostajs/aop-alerts — service alertes (ORM-first) + matching pur. @author Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
import { matchAlert, nextRun } from './match.js';
|
|
3
|
+
export function createAlerts({ repositories, now = () => new Date() } = {}) {
|
|
4
|
+
if (!repositories?.alerts) throw new Error('aop-alerts: repositories.alerts requis (seam ORM)');
|
|
5
|
+
const repo = repositories.alerts;
|
|
6
|
+
async function create(data) { const a = await repo.create({ ...data, nextRunAt: nextRun(data, now()).toISOString() }); return a; }
|
|
7
|
+
/** Évalue une alerte sur une liste d'AO → AO correspondants (triés par score). */
|
|
8
|
+
function run(alert, tenders) {
|
|
9
|
+
return tenders.map((t) => ({ tender: t, ...matchAlert(alert, t) })).filter((r) => r.matches).sort((a, b) => b.score - a.score);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
create, list: () => repo.list(), get: (id) => repo.get(id), update: (id, p) => repo.update(id, p),
|
|
13
|
+
find: (p) => repo.find(p), active: () => repo.find((a) => a.isActive && !a.isPaused),
|
|
14
|
+
match: matchAlert, run, nextRun: (a) => nextRun(a, now()),
|
|
15
|
+
async markRun(id) { const a = await repo.get(id); if (!a) return null; return repo.update(id, { lastRunAt: now().toISOString(), nextRunAt: nextRun(a, now()).toISOString() }); },
|
|
16
|
+
};
|
|
17
|
+
}
|
package/src/index.js
ADDED
package/src/match.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @mostajs/aop-alerts — matching alerte↔AO (LOGIQUE PURE). Port fidèle d'AgoraScope Alert.checkMatch.
|
|
2
|
+
// Poids: keywords 30, cpv 20, budget 15, types 10, procédures 10, localisation 15 ; seuil 30%. @author Dr Hamid MADANI
|
|
3
|
+
const lc = (s) => String(s || '').toLowerCase();
|
|
4
|
+
const has = (arr) => Array.isArray(arr) && arr.length > 0;
|
|
5
|
+
|
|
6
|
+
/** @returns {{ matches:boolean, score:number }} score = pourcentage 0..100 des critères satisfaits. */
|
|
7
|
+
export function matchAlert(alert, tender) {
|
|
8
|
+
const c = alert?.criteria || {};
|
|
9
|
+
const text = `${lc(tender?.title)} ${lc(tender?.description)} ${(tender?.keywords || []).map(lc).join(' ')}`;
|
|
10
|
+
// Exclusions : un seul mot exclu présent ⇒ rejet immédiat.
|
|
11
|
+
if (has(c.excludeKeywords) && c.excludeKeywords.some((k) => text.includes(lc(k)))) return { matches: false, score: 0 };
|
|
12
|
+
let score = 0, maxScore = 0;
|
|
13
|
+
if (has(c.keywords)) { maxScore += 30; const matched = c.keywords.filter((k) => text.includes(lc(k))); if (matched.length) score += (matched.length / c.keywords.length) * 30; }
|
|
14
|
+
if (has(c.cpvCodes)) { maxScore += 20; if ((tender?.cpvCodes || []).some((cpv) => c.cpvCodes.includes(cpv))) score += 20; }
|
|
15
|
+
if (c.budget) { maxScore += 15; const v = tender?.financial?.estimatedValue ?? 0; if ((c.budget.min == null || v >= c.budget.min) && (c.budget.max == null || v <= c.budget.max)) score += 15; }
|
|
16
|
+
if (has(c.types)) { maxScore += 10; if (c.types.includes(tender?.type)) score += 10; }
|
|
17
|
+
if (has(c.procedures)) { maxScore += 10; if (c.procedures.includes(tender?.procedure)) score += 10; }
|
|
18
|
+
if (c.location) { maxScore += 15; const L = c.location, loc = tender?.location || {};
|
|
19
|
+
if (L.departments?.includes(loc.department) || L.regions?.includes(loc.region) || L.cities?.includes(tender?.organization?.address?.city)) score += 15; }
|
|
20
|
+
const finalScore = maxScore > 0 ? (score / maxScore) * 100 : 0;
|
|
21
|
+
return { matches: finalScore >= 30, score: Math.round(finalScore) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Prochaine exécution selon la fréquence (immediate|daily|weekly|monthly). */
|
|
25
|
+
export function nextRun(alert, now = new Date()) {
|
|
26
|
+
const f = alert?.notifications?.frequency || 'daily'; const sch = alert?.notifications?.schedule || {};
|
|
27
|
+
const d = new Date(now);
|
|
28
|
+
if (f === 'immediate') return now;
|
|
29
|
+
const at = (x) => { x.setHours(sch.hour ?? 9, sch.minute ?? 0, 0, 0); return x; };
|
|
30
|
+
if (f === 'daily') { d.setDate(d.getDate() + 1); return at(d); }
|
|
31
|
+
if (f === 'weekly') { const target = sch.dayOfWeek ?? 1; const delta = ((target - d.getDay() + 7) % 7) || 7; d.setDate(d.getDate() + delta); return at(d); }
|
|
32
|
+
if (f === 'monthly') { d.setMonth(d.getMonth() + 1, 1); return at(d); }
|
|
33
|
+
d.setDate(d.getDate() + 1); return at(d);
|
|
34
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function coll(){const m=new Map();let s=0;return{create:async d=>{const id=String(++s),r={id,...d};m.set(id,r);return r;},get:async id=>m.get(id)||null,list:async()=>[...m.values()],find:async p=>[...m.values()].filter(p),update:async(id,patch)=>{const r=m.get(id);if(!r)return null;const u={...r,...patch};m.set(id,u);return u;}};}
|
|
2
|
+
export function createMemoryRepositories(){return{alerts:coll()};}
|
package/src/schemas.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @mostajs/aop-alerts — schéma ORM. @author Dr Hamid MADANI <drmdh@msn.com>
|
|
2
|
+
export const AlertSchema = {
|
|
3
|
+
name: 'Alert', collection: 'alerts', timestamps: true,
|
|
4
|
+
fields: {
|
|
5
|
+
userId: { type: 'string', default: null }, companyId: { type: 'string', default: null },
|
|
6
|
+
name: { type: 'string', required: true }, description: { type: 'text', default: '' },
|
|
7
|
+
criteria: { type: 'json', default: {} }, // { keywords, excludeKeywords, cpvCodes, budget{min,max}, types, procedures, location }
|
|
8
|
+
notifications: { type: 'json', default: { enabled: true, channels: ['email'], frequency: 'daily' } },
|
|
9
|
+
stats: { type: 'json', default: { matchCount: 0, notificationsSent: 0 } },
|
|
10
|
+
isActive: { type: 'boolean', default: true }, isPaused: { type: 'boolean', default: false },
|
|
11
|
+
priority: { type: 'string', enum: ['low', 'normal', 'high'], default: 'normal' },
|
|
12
|
+
lastRunAt: { type: 'date', default: null }, nextRunAt: { type: 'date', default: null },
|
|
13
|
+
},
|
|
14
|
+
indexes: [{ fields: { userId: 'asc' } }, { fields: { isActive: 'asc' } }],
|
|
15
|
+
};
|