@mostajs/aid-grants 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/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog — @mostajs/aid-grants
2
+ ## [0.1.0] — 2026-06-18
3
+ ### Added
4
+ - Aides typées + cycle request/approve/pay/reject + suivi budgétaire (allocated/consumed/remaining) + versement (payment) + stats. 6 tests + exemple §12.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @mostajs/aid-grants
2
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later · **Statut** : 0.1.0 (6 tests verts)
3
+ > Aides accordées + suivi budgétaire + versements. Autonome ; compose payment/numbering ; alimente reporting. Générique NGO.
4
+ ```js
5
+ import { createAidGrants, createMemoryRepositories } from '@mostajs/aid-grants';
6
+ const g = createAidGrants({ repositories: createMemoryRepositories(), payment });
7
+ await g.budget.set('ramadan-2026', 100000);
8
+ const a = await g.aids.request('veuve-12', { type:'financiere', amount:8000, budgetLine:'ramadan-2026' });
9
+ await g.aids.approve(a.id); await g.aids.pay(a.id); // contrôle budget + payment.disburse
10
+ ```
11
+ Lancer : `node test-scripts/unit/aid-grants.test.mjs && node examples/aides/run.mjs`
@@ -0,0 +1,10 @@
1
+ # @mostajs/aid-grants — DEVRULES de bout en bout (§4 + #3 condensés)
2
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Date** : 2026-06-18 · **Statut** : cas C (P3 NGO, générique)
3
+ **Principe** : EXTRACTION « gestion des aides » (asso-sel Modules 1.4/3). Autonome/DB-agnostique.
4
+ ## §4 / §3
5
+ **Aides** typées (financière/alimentaire/parrainage/médicale/scolaire/saisonnière) + cycle (request→approve→pay/reject)
6
+ + **suivi budgétaire** (lignes : allocated/consumed/remaining) + versement. Compose payment (disburse), numbering (n° aide) ;
7
+ alimente @mostajs/reporting (stats). **Frontière** : le bénéficiaire = cœur métier (beneficiaries/medical-cases) ; aid-grants = l'aide+budget.
8
+ **API** : `createAidGrants({repositories,payment?,numbering?})` → `aids.{request,approve,reject,pay,get,listByBeneficiary,byType,byStatus}`,
9
+ `budget.{set,allocated,consumed,remaining}`, `stats({from,to})`.
10
+ **Jalons** : 0.1 cycle+budget+stats · 0.2 payment(disburse) · 1.0 14 livrables.
@@ -0,0 +1,16 @@
1
+ // Exemple §12 — aides (P3 ASSO-SEL) : parrainage + aide saisonnière (budget Ramadan). node examples/aides/run.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { createAidGrants, createMemoryRepositories } from '../../src/index.js';
4
+ const g=createAidGrants({ repositories:createMemoryRepositories(),
5
+ numbering:{ next:()=> 'AID-2026-'+Math.floor(Math.random()*9999) },
6
+ payment:{ async disburse(p){ console.log(' ↪ versé:',p.amount,'DZD →',p.purpose); return {ref:'CCP-'+Date.now()}; } } });
7
+ await g.budget.set('ramadan-2026', 100000);
8
+ // veuve : aide financière + parrainage orphelin
9
+ const a1=await g.aids.request('veuve-12',{ type:'financiere', amount:8000, budgetLine:'ramadan-2026', note:'aide mensuelle' });
10
+ await g.aids.approve(a1.id,{by:'commission'}); await g.aids.pay(a1.id,{by:'compta'});
11
+ const a2=await g.aids.request('orphelin-7',{ type:'parrainage', amount:5000, budgetLine:'ramadan-2026' });
12
+ await g.aids.approve(a2.id); await g.aids.pay(a2.id);
13
+ assert.equal((await g.aids.byStatus('paid')).length,2);
14
+ const st=await g.stats();
15
+ console.log('✅ aid-grants — versé %d DZD · reste budget Ramadan: %d · ventilation %o',
16
+ st.total, await g.budget.remaining('ramadan-2026'), st.byType);
package/llms.txt ADDED
@@ -0,0 +1,13 @@
1
+ # @mostajs/aid-grants — fiche LLM
2
+ RÔLE
3
+ Aides accordées (financière/alimentaire/parrainage/médicale/scolaire/saisonnière) + suivi budgétaire + versements.
4
+ Autonome/DB-agnostique. Compose payment (disburse), numbering (n° aide). Alimente @mostajs/reporting. Générique NGO.
5
+ EXPORTS
6
+ createAidGrants({ repositories, payment?, numbering?, now? }) -> { aids, budget, stats }
7
+ createMemoryRepositories(); AidSchema, BudgetSchema
8
+ API
9
+ aids.request(beneficiaryId,{type,amount,note,budgetLine})/approve(id,{by})/reject/pay(id,{by})/get/listByBeneficiary/byType/byStatus
10
+ budget.set(line,allocated)/allocated(line)/consumed(line)/remaining(line) ; stats({from,to}) -> {total,count,byType}
11
+ PIÈGES
12
+ - cycle: requested→approved→paid | rejected. pay() contrôle le budget (throw 'budget insuffisant') si budgetLine.
13
+ - versement réel = compose payment.disburse. consumed = somme des aides 'paid' d'une ligne.
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@mostajs/aid-grants",
3
+ "version": "0.1.0",
4
+ "description": "Aides accordées (financières/alimentaires/parrainage/médicales/scolaires/saisonnières) + suivi budgétaire + versements. DB-agnostique ; compose payment/numbering ; alimente reporting. Générique NGO.",
5
+ "license": "AGPL-3.0-or-later",
6
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./schemas": "./src/schemas.js"
12
+ },
13
+ "keywords": [
14
+ "mostajs",
15
+ "aid",
16
+ "grants",
17
+ "ngo",
18
+ "budget",
19
+ "welfare"
20
+ ],
21
+ "scripts": {
22
+ "test": "node test-scripts/unit/aid-grants.test.mjs",
23
+ "example": "node examples/aides/run.mjs"
24
+ }
25
+ }
@@ -0,0 +1,50 @@
1
+ // @mostajs/aid-grants — aides accordées + suivi budgétaire. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ // Autonome/DB-agnostique. Compose (injectés) : payment (versement), numbering (n° aide). Alimente @mostajs/reporting.
3
+
4
+ export function createAidGrants({ repositories, payment, numbering, now = () => new Date() } = {}) {
5
+ if (!repositories?.aids) throw new Error('createAidGrants: repositories.aids requis');
6
+ const { aids, budgets } = repositories;
7
+ const ref = () => (numbering?.next ? numbering.next('aid') : `AID-${now().getFullYear()}-${Math.floor(now().getTime() % 1000000)}`);
8
+ const inWindow = ({ from, to } = {}) => (a) => (!from || new Date(a.at) >= new Date(from)) && (!to || new Date(a.at) <= new Date(to));
9
+
10
+ const budget = {
11
+ set: async (line, allocated) => { const ex = (await budgets.find((b) => b.line === line))[0]; return ex ? budgets.update(ex.id, { allocated }) : budgets.create({ line, allocated }); },
12
+ allocated: async (line) => ((await budgets.find((b) => b.line === line))[0]?.allocated || 0),
13
+ consumed: async (line) => (await aids.find((a) => a.budgetLine === line && a.status === 'paid')).reduce((s, a) => s + a.amount, 0),
14
+ async remaining(line) { return (await budget.allocated(line)) - (await budget.consumed(line)); },
15
+ };
16
+
17
+ const api = {
18
+ aids: {
19
+ request: async (beneficiaryId, { type, amount = 0, note, budgetLine } = {}) => {
20
+ if (!type) throw new Error('type d\'aide requis');
21
+ return aids.create({ beneficiaryId, type, amount, note, budgetLine, status: 'requested', ref: ref(), at: now() });
22
+ },
23
+ get: (id) => aids.findById(id),
24
+ approve: (id, { by } = {}) => aids.update(id, { status: 'approved', by }),
25
+ reject: (id, { by } = {}) => aids.update(id, { status: 'rejected', by }),
26
+ /** Versement : contrôle budgétaire + compose @mostajs/payment (disburse). */
27
+ async pay(id, { by } = {}) {
28
+ const a = await aids.findById(id); if (!a) throw new Error('aide introuvable');
29
+ if (a.status === 'paid') return a;
30
+ if (a.status === 'rejected') throw new Error('aide rejetée');
31
+ if (a.budgetLine && a.amount > 0) { const rem = await budget.remaining(a.budgetLine); if (a.amount > rem) throw new Error(`budget insuffisant (${a.budgetLine}: reste ${rem})`); }
32
+ let paymentRef = null;
33
+ if (payment?.disburse) { const r = await payment.disburse({ amount: a.amount, beneficiaryId: a.beneficiaryId, purpose: a.type }); paymentRef = r?.ref ?? r?.id; }
34
+ return aids.update(id, { status: 'paid', paymentRef, by, at: now() });
35
+ },
36
+ listByBeneficiary: (beneficiaryId) => aids.find((a) => a.beneficiaryId === beneficiaryId),
37
+ byType: (type) => aids.find((a) => a.type === type),
38
+ byStatus: (status) => aids.find((a) => a.status === status),
39
+ },
40
+ budget,
41
+ /** Statistiques (alimente @mostajs/reporting) : total versé + ventilation par type. */
42
+ async stats(window) {
43
+ const paid = (await aids.find((a) => a.status === 'paid')).filter(inWindow(window));
44
+ const byType = {}; for (const a of paid) byType[a.type] = (byType[a.type] || 0) + a.amount;
45
+ return { total: paid.reduce((s, a) => s + a.amount, 0), count: paid.length, byType };
46
+ },
47
+ repositories,
48
+ };
49
+ return api;
50
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // @mostajs/aid-grants — point d'entrée. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ export { createAidGrants } from './aid-grants.js';
3
+ export { createMemoryRepositories } from './memory-repo.js';
4
+ export { AidSchema, BudgetSchema } from './schemas.js';
@@ -0,0 +1,7 @@
1
+ // @mostajs/aid-grants — repos mémoire. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ function coll(){ const m=new Map(); return {
3
+ async create(d){ const id=d.id||globalThis.crypto.randomUUID(); const now=new Date(); const r={id,createdAt:now,updatedAt:now,...d}; m.set(id,r); return {...r}; },
4
+ async findById(id){ const r=m.get(id); return r?{...r}:null; },
5
+ async update(id,p){ const r=m.get(id); if(!r)return null; const x={...r,...p,updatedAt:new Date()}; m.set(id,x); return {...x}; },
6
+ async find(f=()=>true){ return [...m.values()].filter(f).map(r=>({...r})); } }; }
7
+ export function createMemoryRepositories(){ return { aids:coll(), budgets:coll() }; }
package/src/schemas.js ADDED
@@ -0,0 +1,10 @@
1
+ // @mostajs/aid-grants — schémas (EntitySchema @mostajs/orm). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ export const AidSchema = { name: 'Aid', collection: 'aids', timestamps: true, fields: {
3
+ beneficiaryId: { type: 'string', required: true },
4
+ type: { type: 'string', enum: ['financiere', 'alimentaire', 'parrainage', 'medicale', 'scolaire', 'saisonniere'], required: true },
5
+ amount: { type: 'number', default: 0 }, note: { type: 'string' }, budgetLine: { type: 'string', default: null },
6
+ status: { type: 'string', enum: ['requested', 'approved', 'paid', 'rejected'], default: 'requested' },
7
+ ref: { type: 'string' }, at: { type: 'date', default: 'now' }, paymentRef: { type: 'string', default: null }, by: { type: 'string', default: null } },
8
+ indexes: [{ fields: { beneficiaryId: 'asc' } }, { fields: { type: 'asc' } }, { fields: { status: 'asc' } }] };
9
+ export const BudgetSchema = { name: 'Budget', collection: 'budgets', timestamps: true, fields: {
10
+ line: { type: 'string', required: true, unique: true }, allocated: { type: 'number', default: 0 } } };
@@ -0,0 +1,42 @@
1
+ // @mostajs/aid-grants — tests (DEVRULES §5). node test-scripts/unit/aid-grants.test.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { createAidGrants, createMemoryRepositories } from '../../src/index.js';
4
+ let pass=0; const test=async(n,f)=>{await f();pass++;console.log(' ✓',n);};
5
+ const mk=(o={})=>createAidGrants({ repositories:createMemoryRepositories(), ...o });
6
+
7
+ await test('cycle : request → approve → pay', async()=>{
8
+ const g=mk(); const a=await g.aids.request('ben1',{type:'financiere',amount:5000});
9
+ assert.equal(a.status,'requested'); assert.ok(a.ref);
10
+ await g.aids.approve(a.id,{by:'agent'}); assert.equal((await g.aids.get(a.id)).status,'approved');
11
+ await g.aids.pay(a.id,{by:'compta'}); assert.equal((await g.aids.get(a.id)).status,'paid');
12
+ });
13
+ await test('type requis ; aide rejetée non payable', async()=>{
14
+ const g=mk(); await assert.rejects(()=>g.aids.request('b',{amount:1}),/type/);
15
+ const a=await g.aids.request('b',{type:'alimentaire',amount:1}); await g.aids.reject(a.id);
16
+ await assert.rejects(()=>g.aids.pay(a.id),/rejet/);
17
+ });
18
+ await test('suivi budgétaire : allocated/consumed/remaining + refus si insuffisant', async()=>{
19
+ const g=mk(); await g.budget.set('ramadan-2026',10000);
20
+ const a1=await g.aids.request('b1',{type:'saisonniere',amount:7000,budgetLine:'ramadan-2026'}); await g.aids.pay(a1.id);
21
+ assert.equal(await g.budget.consumed('ramadan-2026'),7000);
22
+ assert.equal(await g.budget.remaining('ramadan-2026'),3000);
23
+ const a2=await g.aids.request('b2',{type:'saisonniere',amount:5000,budgetLine:'ramadan-2026'});
24
+ await assert.rejects(()=>g.aids.pay(a2.id),/budget insuffisant/);
25
+ });
26
+ await test('versement composé via payment (disburse)', async()=>{
27
+ const out=[]; const g=mk({ payment:{ async disburse(p){out.push(p);return {ref:'PAY-9'};} } });
28
+ const a=await g.aids.request('b',{type:'medicale',amount:3000}); await g.aids.pay(a.id);
29
+ assert.equal((await g.aids.get(a.id)).paymentRef,'PAY-9'); assert.equal(out[0].purpose,'medicale');
30
+ });
31
+ await test('stats : total versé + ventilation par type', async()=>{
32
+ const g=mk();
33
+ for (const [t,m] of [['financiere',1000],['alimentaire',500],['financiere',2000]]) { const a=await g.aids.request('b',{type:t,amount:m}); await g.aids.pay(a.id); }
34
+ const st=await g.stats(); assert.equal(st.total,3500); assert.equal(st.byType.financiere,3000); assert.equal(st.count,3);
35
+ });
36
+ await test('listes par bénéficiaire / type / statut', async()=>{
37
+ const g=mk(); await g.aids.request('b1',{type:'scolaire',amount:1}); const a=await g.aids.request('b1',{type:'medicale',amount:1});
38
+ assert.equal((await g.aids.listByBeneficiary('b1')).length,2);
39
+ assert.equal((await g.aids.byType('medicale')).length,1);
40
+ assert.equal((await g.aids.byStatus('requested')).length,2);
41
+ });
42
+ console.log(`\n✅ @mostajs/aid-grants — ${pass} tests OK`);