npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@primocaredentgroup/compensi-medici-core

v0.4.0

Published

Componente Convex Compensi Medici — motore compensi strutturato, configurabile e integrato per PrimoUpCore

Downloads

267

Readme

@primocaredentgroup/compensi-medici-core

Componente Convex per il motore compensi medici (PrimoUpCore).

API pubblica tipizzata

L'entry principale (.) espone CompensiMediciClient, exposeApi e i validator pubblici. Il riferimento al componente viene passato solo al costruttore/factory: il codice host non deve invocare direttamente le funzioni del raw handle.

import {
  CompensiMediciClient,
  exposeApi,
} from "@primocaredentgroup/compensi-medici-core";
import { components } from "./_generated/api";

const client = new CompensiMediciClient(components.compensiMedici);
await client.configCommissionPlansListActive(ctx, {});

// Helper host server-side condiviso dai wrapper pubblici.
const getActorId = async (ctx) => {
  const actor = await requireAuthenticatedHostActor(ctx);
  return actor.id;
};

// Boundary pubblico host con autorizzazione e actor risolto server-side.
const api = exposeApi(components.compensiMedici, {
  auth: async (ctx, operation) => {
    await requireHostPermission(ctx, operation);
  },
  getActorId,
});

export const createCommissionPlan = api.createCommissionPlan;
export const createCommissionRule = api.createCommissionRule;

getActorId deve derivare l'identità dal contesto autenticato o da una mappatura host attendibile. I campi audit (createdBy, updatedBy, triggeredBy e analoghi) vengono iniettati dal server e non sono argomenti accettati dal frontend. Un userId che identifica il medico o il destinatario del piano resta invece un dato di dominio.

La global setting python_worker_url è riservata al codice server-side: exposeApi ne rifiuta scrittura, rimozione e lettura puntuale e la filtra dagli elenchi, per impedire SSRF indiretta e la divulgazione dell'endpoint interno.

| Subpath | Contenuto | |---------|-----------| | . | CompensiMediciClient, exposeApi, validators re-export | | ./convex.config | registrazione componente | | ./validators | validators pubblici (status, rule types, …) | Il client è intenzionalmente limitato alla superficie supportata e il typecheck verifica ogni argomento e risultato, anche annidato. Non viene generato tramite script regex: nuove operazioni vanno aggiunte esplicitamente insieme al loro contratto e ai test.

DSL canonica delle commission rules

Le nuove scritture host-facing devono passare dai validator esportati commissionRuleConditionsValidator e commissionRuleFormulaValidator. La forma canonica delle condizioni è:

const conditions = {
  serviceCategoryNames: ["Igiene", "Conservativa"],
  listId: ["listino-standard", "listino-premium"],
  serviceRegistryId: "prestazione-123",
};
  • serviceCategoryNames è sempre string[] e viene confrontato con la categoria della prestazione propagata nei metadata di produzione.
  • listId e serviceRegistryId accettano string | string[].
  • Sono inoltre supportati i filtri canonici clinicId, subcategory, conventionId, sourceId, serviceRegistryGroupId, listRowId, hasAgreement, commissionProfileId e serviceCategoryId.
  • Gli alias legacy category, categoryName e serviceId sono accettati in ingresso per compatibilità e normalizzati rispettivamente in serviceCategoryNames e serviceRegistryId. Non vanno usati nelle nuove integrazioni.

Le sole formule canoniche eseguibili dal matcher semplice sono:

const percentageFormula = { type: "percentage", rate: 25 }; // 25%
const fixedFormula = { type: "fixed_amount", amount: 40 };

Per percentage, rate è espresso in punti percentuali: 25 significa 25%, non 0.25. Il campo legacy percentage viene ancora normalizzato a rate, ma non è la forma canonica.

La corrispondenza tra regola e formula è verificata prima della persistenza: ruleType: "percentage" richiede formula.type: "percentage", mentre ruleType: "fixed" richiede formula.type: "fixed_amount". Il solo outputType attualmente ammesso è "commission", perché il motore genera esclusivamente righe ledger di quel tipo. Una formula fixed_amount non accetta rate/percentage: l'importo conserva il segno della produzione/storno ed è moltiplicato soltanto per CPR e per l'eventuale coefficiente Invisalign.

Il boundary rifiuta condizioni prive di semantica nel matcher semplice (role, progressBased, isReimbursement, reimbursementCategory, tokenType) e formule non supportate (fixed_per_unit, percentage_on_progress, token, fixed_global, oltre alle varianti basate su multipliedByWdays). Questa validazione è intenzionale: le regole non vengono salvate se il motore non può valutarle in modo deterministico.

Anche taxProfiles.additionalRules resta un payload storage legacy privo di semantica nel motore: non è accettato né restituito dalla superficie supportata. Una DSL pubblica richiede prima una specifica di dominio e una migrazione dei record esistenti.

Per compatibilità dati con la versione 0.2.0, nello schema persistito commissionRules.conditions e commissionRules.formula restano temporaneamente wide. La superficie host-facing è invece strict e normalizza ogni nuova scrittura. Restringere anche lo storage richiederà una migrazione separata widen -> migrate -> narrow; questa versione non esegue migrazioni.

seedHelpers.clearAll è disponibile esclusivamente tramite CompensiMediciClient da una funzione server-side controllata, per seed/test. Non è esposto da exposeApi e non deve essere raggiungibile dal frontend.

Scope dei run e pending completions

createRun valida la coppia scopeType/scopePayload: users richiede userIds, clinics richiede clinicIds, la selezione non può essere vuota e global non accetta payload. user_clinic_pairs viene rifiutato fail-closed finché orchestrator e worker non lo applicheranno end-to-end. Anche companyId non è esposto nel payload tipizzato: uno scope company nominale sarebbe fuorviante finché il filtro non viene applicato davvero.

triggerPythonCalculation accetta dal boundary host soltanto runId: l'actor è iniettato server-side e gli eventuali user_ids inviati al worker derivano esclusivamente da run.scopePayload.userIds. Il caller non può sostituire lo scope del run.

Anche i due orchestrator TypeScript rifiutano i run legacy user_clinic_pairs e payload scope malformati prima del fetch, così uno scope nominale non può degradare a fetch globale.

I periodi delle API nuove/toccate usano la forma canonica YYYY-MM, con mese 0112; valori come 2026-8, 2026-13 o testo arbitrario vengono rifiutati.

La chiusura usa l'operazione dedicata actor-safe api.closeRun; updateRunStatus non può impostare direttamente closed.

L'orchestrator host prepara il batch con workflowPendingCompletionsPrepareBatchForRun({ runId }) prima di accodare il worker. La mutation seleziona soltanto i pending che corrispondono allo scope global, users o clinics, assegna a ogni riga un ownership durevole (claimedRunId, claimedRevision, claimedAt) e registra sul run la prova della preparazione. Un claim appartenente a un altro run o un run preparato concorrente sullo stesso periodo/scope viene rifiutato fail-closed.

Il limite esplicito è MAX_PENDING_COMPLETIONS_PER_RUN = 500 righe nello scope. La mutation legge al massimo la riga 501 e, se il limite è superato, fallisce prima di qualsiasi patch; non esegue mai un batch parziale. Un run scheduled senza pending restituisce pendingCount=0, non lascia una preparazione e non deve essere accodato. Un run manuale o shadow senza pending è invece un ricalcolo legittimo: conserva una preparazione durevole con count zero e può essere accodato.

enqueueRun, claimRun e triggerCalculation verificano la preparazione prima che il worker parta. markCompleted finalizza nella stessa transaction Convex sia lo stato del run sia tutte e sole le revisioni claimed. Se recordCompletion riceve una modifica economica/source durante il worker, incrementa la revisione e libera il claim: il completamento fallisce senza scritture e il calcolo deve essere ritentato. Un retry identico di recordCompletion resta invece un no-op. La vecchia finalizzazione host markPendingCompletionsProcessed non fa più parte della superficie client/exposeApi supportata.

Gate ancora necessari per il rollout

  • Lo storage legacy mantiene alcuni campi wide (conditions, formula, snapshot e metadata). La superficie supportata li normalizza o li omette, ma restringere lo schema richiede una migrazione separata widen -> migrate -> narrow sui dati reali.
  • Il worker Python deve adottare il payload economico rigoroso e il lifecycle per-attempt di questa versione prima del rollout: claim-run assegna worker_attempt, save-results registra la prova dello stesso tentativo e mark-completed la richiede.
  • Gli scope user_clinic_pairs e company restano intenzionalmente fail-closed finché non esiste una semantica end-to-end approvata.
  • Matching shadow con il sistema legacy, test multi-clinica/multi-company e verbale E2E restano gate di rilascio operativi, non scorciatoie da risolvere nel client.

Il package non va dichiarato operativo in produzione finché il worker coordinato e i gate E2E non sono completati.

Worker HTTP — adapter host

Il package non esporta route HTTP Convex da montare direttamente nel deploy host. L'host possiede autenticazione, environment e routing e monta il proprio adapter in compensiWorkerRoutes.ts, riusando dal package il parser tipizzato del payload economico. Non importare file interni convex/http/* del package.

L'host monta le route in compensiWorkerRoutes.ts:

import { registerCompensiWorkerRoutes } from "./compensiWorkerRoutes";

registerCompensiWorkerRoutes(http);

Worker Integration API

API HTTP worker-only per il Python worker su Railway. Il componente Convex resta source of truth; il worker è solo esecutore esterno.

Autenticazione

Tutte le richieste richiedono:

Authorization: Bearer <WORKER_SECRET>

Configura WORKER_SECRET nelle variabili d'ambiente del deployment Convex (PrimoUpCore).

Registrazione route (PrimoUpCore)

Vedi sezione Worker HTTP sopra. Dettagli aggiuntivi in convex/http/INTEGRATION.md.

Base URL: https://<deployment>.convex.site

Endpoint

| Metodo | Path | Descrizione | |--------|------|-------------| | POST | /worker/get-next-run | Primo run con workerStatus=queued (FIFO) | | POST | /worker/claim-run | Claim atomico → processing | | POST | /worker/update-progress | Aggiorna progress / progressMessage | | POST | /worker/save-results | Salva ledger, fatture, issues, summary | | POST | /worker/mark-completed | workerStatus=completed, business status=calculated | | POST | /worker/mark-failed | workerStatus=failed + errore |

Flow esecuzione

enqueue (UI) → queued
     ↓
get-next-run → claim-run → processing
     ↓
update-progress (opzionale, ripetibile)
     ↓
save-results (solo in processing)
     ↓
mark-completed → completed + status business "calculated"

In caso di errore: mark-failedfailed.

Esempi payload

get-next-run — body vuoto {}

{ "run": { "id": "...", "period": "2025-04", "status": "queued", "created_at": 1710000000 } }

oppure { "run": null }.

claim-run

{ "runId": "jh7...", "workerId": "railway-worker-1" }

La risposta contiene il token di fencing da riusare in tutte le scritture:

{
  "workerAttempt": 3,
  "run": { "id": "jh7...", "worker_id": "railway-worker-1", "worker_attempt": 3 }
}

update-progress

{
  "runId": "jh7...",
  "workerId": "railway-worker-1",
  "workerAttempt": 3,
  "progress": 45,
  "message": "Calcolo commissioni..."
}

save-results

{
  "runId": "jh7...",
  "workerId": "railway-worker-1",
  "workerAttempt": 3,
  "result": {
    "summary": {
      "totale_medici": 2,
      "totale_lordo": 7300,
      "totale_netto": 5840,
      "valuta": "EUR"
    },
    "ledger_entries": [
      {
        "medico_id": "u1",
        "clinic_id": "c1",
        "company_id": "company-source",
        "outside_company_type": "None",
        "type": "commission",
        "importo_netto": 3360,
        "descrizione": "Commissioni aprile"
      }
    ],
    "draft_invoices": [
      {
        "medico_id": "u1",
        "clinic_id": "c1",
        "company_id": "company-source",
        "outside_company_type": "None",
        "line_item_count": 1,
        "net_price": 3360,
        "subtotal": 3360,
        "vat_rate": 0,
        "vat_amount": 0,
        "withholding_rate": 0,
        "withholding_on_rate": 0,
        "withholding_amount": 0,
        "contribution_rate": 0,
        "contribution_amount": 0,
        "stamp_duty_amount": 0,
        "total": 3360,
        "net_to_pay": 3360
      }
    ],
    "issues": [
      { "code": "MISSING_TIMESHEET", "severity": "warning", "message": "..." }
    ]
  }
}

mark-completed

{
  "runId": "jh7...",
  "workerId": "railway-worker-1",
  "workerAttempt": 3
}

mark-failed

{
  "runId": "jh7...",
  "workerId": "railway-worker-1",
  "workerAttempt": 3,
  "errorMessage": "Timeout calcolo",
  "errorStack": "..."
}

Accodare un run (wrapper host)

import { CompensiMediciClient } from "@primocaredentgroup/compensi-medici-core";
import { components } from "./_generated/api";

const compensi = new CompensiMediciClient(components.compensiMedici);

// Dentro una mutation host autenticata; `runId` arriva dagli args,
// l'actor no: viene risolto server-side dal contesto.
const actorId = await getActorId(ctx);
await compensi.workflowWorkerApiEnqueueRun(ctx, {
  runId,
  userId: actorId,
});

Il userId richiesto dall'API worker è l'actor audit della transizione, quindi non deve provenire dal payload del caller. Il run deve essere in stato business draft (o non closed). Imposta workerStatus: "queued".

Modello dati worker vs business

| Campo | Significato | |-------|-------------| | workerStatus | Coda worker: queuedprocessingcompleted / failed | | status | Workflow business: draftcalculatedreviewingapprovedclosed |

Il worker vede status nel JSON HTTP = workerStatus. Al completamento, Convex imposta anche status: "calculated".

Idempotenza

  • claim: stesso workerId su run già processing → OK (ritorna il run).
  • claim-run: solo con business status=draft; assegna/incrementa worker_attempt, lo restituisce come workerAttempt e un retry dello stesso worker conserva il tentativo attivo.
  • update-progress / save-results / mark-completed / mark-failed: richiedono sempre il workerId e il workerAttempt restituiti dal claim corrente. Una coppia obsoleta viene rifiutata prima di qualsiasi scrittura.
  • save-results: solo con workerStatus=processing e business status=draft; valida identità, scope e numeri finiti prima di sostituire gli output auto-generati e registra il tentativo salvato. Ogni ledger e fattura deve dichiarare sia company_id sia outside_company_type (None, DS, Est, Est_forfettari); non esiste un default companyless. Est richiede anche invoice_vat. Convex verifica profilo e formule, applica i company mapping attivi e ricostruisce sempre commissionIds dagli ID Convex appena inseriti: il worker non può inviarli. Le issue che impediscono una fattura per profilo fiscale vengono accettate soltanto se coincidono con la risoluzione del profilo effettuata dal componente.
  • mark-completed: riesce solo da business status=draft e se save-results è terminato nello stesso workerAttempt; non può sovrascrivere reviewing, approved o closed. Un retry già completato è idempotente soltanto con la stessa coppia di claim.
  • mark-failed: riesce solo da processing con business status=draft; un retry già fallito è idempotente soltanto con la stessa coppia di claim.

Variabili Railway (worker Python)

CONVEX_HTTP_URL=https://<deployment>.convex.site
WORKER_SECRET=<stesso valore di Convex>
WORKER_ID=railway-worker-1
USE_MOCK_API=false

Worker Monitor (DEV example)

UI tecnica per debugging runtime del worker — non produzione.

Vedi example/README.md per setup Vite + mini-host Convex.

API nel componente (convex/workflow/workerMonitor.ts):

  • Query: listWorkerRuns, getWorkerMetrics, getWorkerRunDetails
  • Dev mutations: enqueueRunDev, retryRunDev, resetWorkerStatusDev, createFakeFailedRunDev, createFakeProcessingStuckRunDev

Helper: convex/lib/workerMonitorHelpers.ts (duration, stuck detection, metrics).