@primocaredentgroup/compensi-medici-core 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/convex/_generated/api.ts +110 -0
- package/convex/_generated/component.ts +1356 -0
- package/convex/_generated/dataModel.ts +60 -0
- package/convex/_generated/server.ts +156 -0
- package/convex/audit/logs.ts +69 -0
- package/convex/calculation/commissions.ts +497 -0
- package/convex/calculation/deductions.ts +119 -0
- package/convex/calculation/engine.ts +598 -0
- package/convex/calculation/fixedCommissions.ts +217 -0
- package/convex/calculation/orchestrator.ts +314 -0
- package/convex/calculation/production.ts +495 -0
- package/convex/calculation/productionData.ts +155 -0
- package/convex/calculation/ruleApplications.ts +121 -0
- package/convex/config/categories.ts +114 -0
- package/convex/config/commissionPlans.ts +166 -0
- package/convex/config/commissionRules.ts +154 -0
- package/convex/config/companyMappings.ts +77 -0
- package/convex/config/deductionRules.ts +113 -0
- package/convex/config/fixedCommissionTypes.ts +79 -0
- package/convex/config/globalSettings.ts +79 -0
- package/convex/config/invisalignConfig.ts +87 -0
- package/convex/config/planTemplates.ts +90 -0
- package/convex/config/taxProfiles.ts +122 -0
- package/convex/config/userTaxProfiles.ts +87 -0
- package/convex/convex.config.ts +3 -0
- package/convex/documents/draftInvoices.ts +164 -0
- package/convex/documents/invoiceEngine.ts +468 -0
- package/convex/documents/invoiceGeneration.ts +611 -0
- package/convex/documents/statements.ts +185 -0
- package/convex/ledger/entries.ts +314 -0
- package/convex/lib/types.ts +126 -0
- package/convex/schema.ts +611 -0
- package/convex/seedHelpers.ts +41 -0
- package/convex/workflow/pendingCompletions.ts +148 -0
- package/convex/workflow/pythonWorker.ts +190 -0
- package/convex/workflow/runIssues.ts +364 -0
- package/convex/workflow/runs.ts +718 -0
- package/package.json +43 -0
package/convex/schema.ts
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────
|
|
5
|
+
// Validators riutilizzabili (esportati per le funzioni backend)
|
|
6
|
+
// ─────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export const planStatus = v.union(
|
|
9
|
+
v.literal("draft"),
|
|
10
|
+
v.literal("active"),
|
|
11
|
+
v.literal("archived"),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const ruleType = v.union(
|
|
15
|
+
v.literal("percentage"),
|
|
16
|
+
v.literal("fixed"),
|
|
17
|
+
v.literal("token"),
|
|
18
|
+
v.literal("combined"),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const ruleOutputType = v.union(
|
|
22
|
+
v.literal("commission"),
|
|
23
|
+
v.literal("fixed"),
|
|
24
|
+
v.literal("bonus"),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export const runStatus = v.union(
|
|
28
|
+
v.literal("draft"),
|
|
29
|
+
v.literal("calculated"),
|
|
30
|
+
v.literal("reviewing"),
|
|
31
|
+
v.literal("approved"),
|
|
32
|
+
v.literal("closed"),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const scopeType = v.union(
|
|
36
|
+
v.literal("global"),
|
|
37
|
+
v.literal("users"),
|
|
38
|
+
v.literal("clinics"),
|
|
39
|
+
v.literal("user_clinic_pairs"),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const calculationMode = v.union(
|
|
43
|
+
v.literal("manual"),
|
|
44
|
+
v.literal("scheduled"),
|
|
45
|
+
v.literal("reopen"),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const ledgerEntryType = v.union(
|
|
49
|
+
v.literal("production"),
|
|
50
|
+
v.literal("storno"),
|
|
51
|
+
v.literal("commission"),
|
|
52
|
+
v.literal("fixed"),
|
|
53
|
+
v.literal("bonus"),
|
|
54
|
+
v.literal("reimbursement"),
|
|
55
|
+
v.literal("adjustment"),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export const sourceType = v.union(
|
|
59
|
+
v.literal("care_plan_row"),
|
|
60
|
+
v.literal("storno"),
|
|
61
|
+
v.literal("manual"),
|
|
62
|
+
v.literal("system"),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
export const invoiceStatus = v.union(
|
|
66
|
+
v.literal("draft"),
|
|
67
|
+
v.literal("ready"),
|
|
68
|
+
v.literal("sent"),
|
|
69
|
+
v.literal("cancelled"),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export const categoryKind = v.union(
|
|
73
|
+
v.literal("reimbursement"),
|
|
74
|
+
v.literal("adjustment"),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export const issueSeverity = v.union(
|
|
78
|
+
v.literal("error"),
|
|
79
|
+
v.literal("warning"),
|
|
80
|
+
v.literal("info"),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
export const issueType = v.union(
|
|
84
|
+
v.literal("no_active_plan"),
|
|
85
|
+
v.literal("no_matching_rule"),
|
|
86
|
+
v.literal("no_tax_profile"),
|
|
87
|
+
v.literal("multiple_tax_profiles"),
|
|
88
|
+
v.literal("inactive_tax_profile"),
|
|
89
|
+
v.literal("fiscal_anomaly"),
|
|
90
|
+
v.literal("missing_invoices"),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// ─────────────────────────────────────────────────────────
|
|
94
|
+
// Schema
|
|
95
|
+
// ─────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export default defineSchema({
|
|
98
|
+
// ── CONFIG ──────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Template di piani provvigionali.
|
|
102
|
+
* Da analisi.md: "basato su template", "importabile da CSV/Excel".
|
|
103
|
+
* I template servono come modello per creare nuovi piani velocemente.
|
|
104
|
+
*/
|
|
105
|
+
commissionPlanTemplates: defineTable({
|
|
106
|
+
code: v.string(),
|
|
107
|
+
name: v.string(),
|
|
108
|
+
description: v.optional(v.string()),
|
|
109
|
+
/** Struttura di regole predefinite incluse nel template */
|
|
110
|
+
defaultRules: v.optional(v.any()),
|
|
111
|
+
isActive: v.boolean(),
|
|
112
|
+
createdBy: v.string(),
|
|
113
|
+
createdAt: v.number(),
|
|
114
|
+
updatedAt: v.number(),
|
|
115
|
+
})
|
|
116
|
+
.index("by_code", ["code"])
|
|
117
|
+
.index("by_active", ["isActive"]),
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Piano provvigionale associato a un medico.
|
|
121
|
+
* Da analisi.md: "ogni piano è associato a un medico,
|
|
122
|
+
* può variare per clinica, categoria, prestazione, convenzione".
|
|
123
|
+
*
|
|
124
|
+
* Ciclo di vita: draft → active → archived.
|
|
125
|
+
* Validità temporale tramite effectiveFrom / effectiveTo.
|
|
126
|
+
*/
|
|
127
|
+
commissionPlans: defineTable({
|
|
128
|
+
userId: v.string(),
|
|
129
|
+
code: v.string(),
|
|
130
|
+
name: v.string(),
|
|
131
|
+
description: v.optional(v.string()),
|
|
132
|
+
status: planStatus,
|
|
133
|
+
effectiveFrom: v.number(),
|
|
134
|
+
effectiveTo: v.optional(v.number()),
|
|
135
|
+
templateId: v.optional(v.id("commissionPlanTemplates")),
|
|
136
|
+
notes: v.optional(v.string()),
|
|
137
|
+
createdBy: v.string(),
|
|
138
|
+
createdAt: v.number(),
|
|
139
|
+
updatedBy: v.string(),
|
|
140
|
+
updatedAt: v.number(),
|
|
141
|
+
})
|
|
142
|
+
.index("by_user", ["userId"])
|
|
143
|
+
.index("by_user_status", ["userId", "status"])
|
|
144
|
+
.index("by_code", ["code"])
|
|
145
|
+
.index("by_status", ["status"]),
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Regola di calcolo all'interno di un piano.
|
|
149
|
+
* Struttura logica da analisi.md:
|
|
150
|
+
* 1. Matching (conditions) — quando si applica
|
|
151
|
+
* 2. Formula — percentuale / fisso / gettone / combinazioni
|
|
152
|
+
* 3. Trattamenti economici — deduzioni, esclusioni, moltiplicatori
|
|
153
|
+
*
|
|
154
|
+
* conditions e formula restano JSON per flessibilità massima,
|
|
155
|
+
* ma i campi espliciti (ruleType, outputType, ecc.) danno
|
|
156
|
+
* semantica leggibile senza dover ispezionare il JSON.
|
|
157
|
+
*/
|
|
158
|
+
commissionRules: defineTable({
|
|
159
|
+
planId: v.id("commissionPlans"),
|
|
160
|
+
name: v.string(),
|
|
161
|
+
ruleType: ruleType,
|
|
162
|
+
priority: v.number(),
|
|
163
|
+
isActive: v.boolean(),
|
|
164
|
+
/** Tipo di riga ledger prodotta dall'applicazione di questa regola */
|
|
165
|
+
outputType: ruleOutputType,
|
|
166
|
+
/** Etichetta leggibile che appare nel prospetto / spiegazione */
|
|
167
|
+
explanationLabel: v.string(),
|
|
168
|
+
conditions: v.any(),
|
|
169
|
+
formula: v.any(),
|
|
170
|
+
validFrom: v.optional(v.number()),
|
|
171
|
+
validTo: v.optional(v.number()),
|
|
172
|
+
createdBy: v.string(),
|
|
173
|
+
createdAt: v.number(),
|
|
174
|
+
updatedAt: v.number(),
|
|
175
|
+
})
|
|
176
|
+
.index("by_plan", ["planId", "priority"])
|
|
177
|
+
.index("by_plan_active", ["planId", "isActive"]),
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Categorie per rimborsi e rettifiche — configurabili da DB.
|
|
181
|
+
* Da analisi.md: rimborsi e rettifiche hanno "categoria, importo,
|
|
182
|
+
* segno (+/-), autore, motivazione".
|
|
183
|
+
* Le categorie sono entità a sé, non costanti hardcoded.
|
|
184
|
+
*/
|
|
185
|
+
reimbursementCategories: defineTable({
|
|
186
|
+
code: v.string(),
|
|
187
|
+
label: v.string(),
|
|
188
|
+
description: v.optional(v.string()),
|
|
189
|
+
isActive: v.boolean(),
|
|
190
|
+
sortOrder: v.number(),
|
|
191
|
+
createdAt: v.number(),
|
|
192
|
+
updatedAt: v.number(),
|
|
193
|
+
})
|
|
194
|
+
.index("by_code", ["code"])
|
|
195
|
+
.index("by_active", ["isActive", "sortOrder"]),
|
|
196
|
+
|
|
197
|
+
adjustmentCategories: defineTable({
|
|
198
|
+
code: v.string(),
|
|
199
|
+
label: v.string(),
|
|
200
|
+
description: v.optional(v.string()),
|
|
201
|
+
isActive: v.boolean(),
|
|
202
|
+
sortOrder: v.number(),
|
|
203
|
+
createdAt: v.number(),
|
|
204
|
+
updatedAt: v.number(),
|
|
205
|
+
})
|
|
206
|
+
.index("by_code", ["code"])
|
|
207
|
+
.index("by_active", ["isActive", "sortOrder"]),
|
|
208
|
+
|
|
209
|
+
taxProfiles: defineTable({
|
|
210
|
+
code: v.string(),
|
|
211
|
+
name: v.string(),
|
|
212
|
+
description: v.optional(v.string()),
|
|
213
|
+
vatRatePercent: v.number(),
|
|
214
|
+
withholdingRatePercent: v.number(),
|
|
215
|
+
withholdingOnPercent: v.number(),
|
|
216
|
+
contributionRatePercent: v.number(),
|
|
217
|
+
contributionWithholding: v.boolean(),
|
|
218
|
+
contributionText: v.optional(v.string()),
|
|
219
|
+
stampDutyEnabled: v.boolean(),
|
|
220
|
+
stampDutyAmount: v.number(),
|
|
221
|
+
stampDutyThreshold: v.number(),
|
|
222
|
+
is104: v.boolean(),
|
|
223
|
+
isBolloCompreso: v.boolean(),
|
|
224
|
+
footerText: v.optional(v.string()),
|
|
225
|
+
additionalRules: v.optional(v.any()),
|
|
226
|
+
isActive: v.boolean(),
|
|
227
|
+
createdBy: v.string(),
|
|
228
|
+
createdAt: v.number(),
|
|
229
|
+
updatedAt: v.number(),
|
|
230
|
+
})
|
|
231
|
+
.index("by_code", ["code"])
|
|
232
|
+
.index("by_active", ["isActive"]),
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Assegnazione profilo fiscale a un medico nel tempo.
|
|
236
|
+
* Un medico può cambiare regime fiscale; questa tabella
|
|
237
|
+
* traccia quale profilo è valido in quale periodo.
|
|
238
|
+
*/
|
|
239
|
+
userTaxProfiles: defineTable({
|
|
240
|
+
userId: v.string(),
|
|
241
|
+
taxProfileId: v.id("taxProfiles"),
|
|
242
|
+
effectiveFrom: v.number(),
|
|
243
|
+
effectiveTo: v.optional(v.number()),
|
|
244
|
+
notes: v.optional(v.string()),
|
|
245
|
+
createdBy: v.string(),
|
|
246
|
+
createdAt: v.number(),
|
|
247
|
+
})
|
|
248
|
+
.index("by_user", ["userId"])
|
|
249
|
+
.index("by_user_effective", ["userId", "effectiveFrom"]),
|
|
250
|
+
|
|
251
|
+
// ── DEDUCTION RULES ─────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
deductionRules: defineTable({
|
|
254
|
+
code: v.string(),
|
|
255
|
+
label: v.string(),
|
|
256
|
+
description: v.optional(v.string()),
|
|
257
|
+
conditions: v.object({
|
|
258
|
+
companyIds: v.optional(v.array(v.string())),
|
|
259
|
+
carePlanTypeIds: v.optional(v.array(v.number())),
|
|
260
|
+
isInsurance: v.optional(v.boolean()),
|
|
261
|
+
isSinistro: v.optional(v.boolean()),
|
|
262
|
+
percentageThreshold: v.optional(v.number()),
|
|
263
|
+
percentageAboveThreshold: v.optional(v.boolean()),
|
|
264
|
+
conventionNameContains: v.optional(v.string()),
|
|
265
|
+
serviceCategoryNames: v.optional(v.array(v.string())),
|
|
266
|
+
}),
|
|
267
|
+
deductionPercent: v.number(),
|
|
268
|
+
priority: v.number(),
|
|
269
|
+
isActive: v.boolean(),
|
|
270
|
+
createdBy: v.string(),
|
|
271
|
+
createdAt: v.number(),
|
|
272
|
+
updatedAt: v.number(),
|
|
273
|
+
})
|
|
274
|
+
.index("by_code", ["code"])
|
|
275
|
+
.index("by_active_priority", ["isActive", "priority"]),
|
|
276
|
+
|
|
277
|
+
// ── COMPANY MAPPINGS ───────────────────────────────────
|
|
278
|
+
|
|
279
|
+
companyMappings: defineTable({
|
|
280
|
+
fromCompanyId: v.string(),
|
|
281
|
+
toCompanyId: v.string(),
|
|
282
|
+
category: v.union(
|
|
283
|
+
v.literal("DS"),
|
|
284
|
+
v.literal("Est"),
|
|
285
|
+
v.literal("Est_forfettari"),
|
|
286
|
+
),
|
|
287
|
+
isActive: v.boolean(),
|
|
288
|
+
createdAt: v.number(),
|
|
289
|
+
updatedAt: v.number(),
|
|
290
|
+
})
|
|
291
|
+
.index("by_category", ["category", "isActive"])
|
|
292
|
+
.index("by_from", ["fromCompanyId"]),
|
|
293
|
+
|
|
294
|
+
// ── GLOBAL SETTINGS ────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
globalSettings: defineTable({
|
|
297
|
+
key: v.string(),
|
|
298
|
+
value: v.string(),
|
|
299
|
+
description: v.optional(v.string()),
|
|
300
|
+
updatedBy: v.optional(v.string()),
|
|
301
|
+
updatedAt: v.number(),
|
|
302
|
+
})
|
|
303
|
+
.index("by_key", ["key"]),
|
|
304
|
+
|
|
305
|
+
// ── FIXED COMMISSION TYPES ─────────────────────────────
|
|
306
|
+
|
|
307
|
+
fixedCommissionTypes: defineTable({
|
|
308
|
+
code: v.string(),
|
|
309
|
+
name: v.string(),
|
|
310
|
+
description: v.optional(v.string()),
|
|
311
|
+
behavior: v.union(
|
|
312
|
+
v.literal("minimum_guaranteed"),
|
|
313
|
+
v.literal("additive"),
|
|
314
|
+
v.literal("substitutive"),
|
|
315
|
+
v.literal("substitutive_no_wdays"),
|
|
316
|
+
),
|
|
317
|
+
multipliedByWdays: v.boolean(),
|
|
318
|
+
isActive: v.boolean(),
|
|
319
|
+
createdAt: v.number(),
|
|
320
|
+
updatedAt: v.number(),
|
|
321
|
+
})
|
|
322
|
+
.index("by_code", ["code"])
|
|
323
|
+
.index("by_active", ["isActive"]),
|
|
324
|
+
|
|
325
|
+
// ── INVISALIGN CONFIG ──────────────────────────────────
|
|
326
|
+
|
|
327
|
+
invisalignConfig: defineTable({
|
|
328
|
+
listRowId: v.string(),
|
|
329
|
+
firstPhasePercentage: v.number(),
|
|
330
|
+
otherPhasesPercentage: v.number(),
|
|
331
|
+
firstPhaseMin: v.number(),
|
|
332
|
+
otherPhasesMin: v.number(),
|
|
333
|
+
isActive: v.boolean(),
|
|
334
|
+
createdAt: v.number(),
|
|
335
|
+
updatedAt: v.number(),
|
|
336
|
+
})
|
|
337
|
+
.index("by_listRowId", ["listRowId"])
|
|
338
|
+
.index("by_active", ["isActive"]),
|
|
339
|
+
|
|
340
|
+
invisalignMinUsers: defineTable({
|
|
341
|
+
userId: v.string(),
|
|
342
|
+
isActive: v.boolean(),
|
|
343
|
+
createdAt: v.number(),
|
|
344
|
+
})
|
|
345
|
+
.index("by_userId", ["userId"]),
|
|
346
|
+
|
|
347
|
+
// ── WORKFLOW ────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Run mensile — ciclo di calcolo compensi per un periodo.
|
|
351
|
+
* Da analisi.md:
|
|
352
|
+
* - Mese aperto: sempre ricalcolabile, modificabile
|
|
353
|
+
* - Mese chiuso: snapshot immutabile, riapertura crea nuova versione
|
|
354
|
+
*
|
|
355
|
+
* Shadow mode (Fase 2 migrazione):
|
|
356
|
+
* "Convex in shadow mode: stesso input, stessi mesi, nessun impatto operativo"
|
|
357
|
+
*
|
|
358
|
+
* scopeType + scopePayload definiscono il perimetro del run:
|
|
359
|
+
* può essere globale o ristretto a specifici medici/cliniche.
|
|
360
|
+
*/
|
|
361
|
+
compensationRuns: defineTable({
|
|
362
|
+
period: v.string(),
|
|
363
|
+
status: runStatus,
|
|
364
|
+
version: v.number(),
|
|
365
|
+
isShadowRun: v.boolean(),
|
|
366
|
+
basedOnRunId: v.optional(v.id("compensationRuns")),
|
|
367
|
+
scopeType: scopeType,
|
|
368
|
+
scopePayload: v.optional(v.any()),
|
|
369
|
+
calculationMode: calculationMode,
|
|
370
|
+
triggeredBy: v.string(),
|
|
371
|
+
notes: v.optional(v.string()),
|
|
372
|
+
createdAt: v.number(),
|
|
373
|
+
updatedAt: v.number(),
|
|
374
|
+
closedAt: v.optional(v.number()),
|
|
375
|
+
closedBy: v.optional(v.string()),
|
|
376
|
+
/**
|
|
377
|
+
* Snapshot dei totali al momento della chiusura.
|
|
378
|
+
* Da analisi.md: "snapshot immutabile" alla chiusura del mese.
|
|
379
|
+
* Garantisce accesso rapido ai dati ufficiali senza ricalcolo.
|
|
380
|
+
*/
|
|
381
|
+
closureSummary: v.optional(v.any()),
|
|
382
|
+
})
|
|
383
|
+
.index("by_period", ["period"])
|
|
384
|
+
.index("by_period_version", ["period", "version"])
|
|
385
|
+
.index("by_status", ["status"])
|
|
386
|
+
.index("by_shadow", ["isShadowRun", "period"]),
|
|
387
|
+
|
|
388
|
+
// ── LEDGER ─────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Riga del ledger compensi — CORE del sistema.
|
|
392
|
+
* Da analisi.md: "il sistema NON salva solo totali.
|
|
393
|
+
* Ogni mese per medico viene costruito un ledger di righe."
|
|
394
|
+
*
|
|
395
|
+
* Fonte di verità per: prospetto, fattura, audit, storico.
|
|
396
|
+
*
|
|
397
|
+
* Campi espliciti per gli aspetti chiave del dominio:
|
|
398
|
+
* - sourceType + sourceId: da dove arriva la riga
|
|
399
|
+
* - planId + ruleId: quale piano/regola l'ha generata (se calcolata)
|
|
400
|
+
* - quantity + unitAmount: dettaglio importo (es. 3 prestazioni × 50€)
|
|
401
|
+
* - isManual: distingue righe inserite a mano da quelle calcolate
|
|
402
|
+
* - effectiveDate: data di competenza economica
|
|
403
|
+
* - period: periodo di riferimento (ridondante col run, ma utile per query dirette)
|
|
404
|
+
*
|
|
405
|
+
* metadata resta per informazioni accessorie non strutturate.
|
|
406
|
+
*/
|
|
407
|
+
ledgerEntries: defineTable({
|
|
408
|
+
runId: v.id("compensationRuns"),
|
|
409
|
+
userId: v.string(),
|
|
410
|
+
clinicId: v.string(),
|
|
411
|
+
type: ledgerEntryType,
|
|
412
|
+
sourceType: sourceType,
|
|
413
|
+
sourceId: v.optional(v.string()),
|
|
414
|
+
planId: v.optional(v.id("commissionPlans")),
|
|
415
|
+
ruleId: v.optional(v.id("commissionRules")),
|
|
416
|
+
scope: v.optional(v.string()),
|
|
417
|
+
fixedCommissionTypeId: v.optional(v.id("fixedCommissionTypes")),
|
|
418
|
+
companyId: v.optional(v.string()),
|
|
419
|
+
description: v.string(),
|
|
420
|
+
amount: v.number(),
|
|
421
|
+
quantity: v.optional(v.number()),
|
|
422
|
+
unitAmount: v.optional(v.number()),
|
|
423
|
+
isManual: v.boolean(),
|
|
424
|
+
effectiveDate: v.number(),
|
|
425
|
+
period: v.string(),
|
|
426
|
+
metadata: v.optional(v.any()),
|
|
427
|
+
createdBy: v.string(),
|
|
428
|
+
createdAt: v.number(),
|
|
429
|
+
})
|
|
430
|
+
.index("by_run", ["runId"])
|
|
431
|
+
.index("by_run_user", ["runId", "userId"])
|
|
432
|
+
.index("by_run_user_clinic", ["runId", "userId", "clinicId"])
|
|
433
|
+
.index("by_run_type", ["runId", "type"])
|
|
434
|
+
.index("by_period_user", ["period", "userId"])
|
|
435
|
+
.index("by_source", ["sourceType", "sourceId"]),
|
|
436
|
+
|
|
437
|
+
// ── DOCUMENTS ──────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Bozza fattura generata per un medico in un run.
|
|
441
|
+
* Da analisi.md: "generazione bozza fattura".
|
|
442
|
+
*
|
|
443
|
+
* Campi fiscali predisposti per il futuro motore fiscale.
|
|
444
|
+
* Da analisi.md: "il piano dice quanto guadagni,
|
|
445
|
+
* il profilo fiscale dice come lo fatturi".
|
|
446
|
+
* Gestisce: IVA, ritenuta, contributi, bollo.
|
|
447
|
+
*/
|
|
448
|
+
draftInvoices: defineTable({
|
|
449
|
+
runId: v.id("compensationRuns"),
|
|
450
|
+
userId: v.string(),
|
|
451
|
+
clinicId: v.string(),
|
|
452
|
+
companyId: v.optional(v.string()),
|
|
453
|
+
period: v.string(),
|
|
454
|
+
status: invoiceStatus,
|
|
455
|
+
isAutoGenerated: v.boolean(),
|
|
456
|
+
outsideCompanyType: v.optional(v.string()),
|
|
457
|
+
isCompanyGrouped: v.boolean(),
|
|
458
|
+
taxProfileId: v.optional(v.id("taxProfiles")),
|
|
459
|
+
taxProfileSnapshot: v.optional(v.any()),
|
|
460
|
+
lineItemCount: v.number(),
|
|
461
|
+
netPrice: v.number(),
|
|
462
|
+
subtotal: v.number(),
|
|
463
|
+
vatRate: v.number(),
|
|
464
|
+
vatAmount: v.number(),
|
|
465
|
+
withholdingRate: v.number(),
|
|
466
|
+
withholdingOnRate: v.number(),
|
|
467
|
+
withholdingAmount: v.number(),
|
|
468
|
+
contributionRate: v.number(),
|
|
469
|
+
contributionAmount: v.number(),
|
|
470
|
+
stampDutyAmount: v.number(),
|
|
471
|
+
total: v.number(),
|
|
472
|
+
netToPay: v.number(),
|
|
473
|
+
commissionIds: v.optional(v.array(v.string())),
|
|
474
|
+
cprIds: v.optional(v.array(v.string())),
|
|
475
|
+
notes: v.optional(v.string()),
|
|
476
|
+
createdBy: v.string(),
|
|
477
|
+
createdAt: v.number(),
|
|
478
|
+
updatedAt: v.number(),
|
|
479
|
+
})
|
|
480
|
+
.index("by_run", ["runId"])
|
|
481
|
+
.index("by_run_user", ["runId", "userId"])
|
|
482
|
+
.index("by_run_user_clinic", ["runId", "userId", "clinicId"])
|
|
483
|
+
.index("by_period_user", ["period", "userId"])
|
|
484
|
+
.index("by_status", ["status"]),
|
|
485
|
+
|
|
486
|
+
// ── EXPLAINABILITY ─────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Applicazione di una regola a una riga del ledger.
|
|
490
|
+
* Tabella dedicata per la spiegabilità del calcolo.
|
|
491
|
+
*
|
|
492
|
+
* Da analisi.md:
|
|
493
|
+
* - "spiegazione delle regole applicate"
|
|
494
|
+
* - "tutto spiegabile"
|
|
495
|
+
* - "differenze spiegabili" (criterio di successo)
|
|
496
|
+
*
|
|
497
|
+
* Salva per ogni riga calcolata:
|
|
498
|
+
* - quale piano è stato usato
|
|
499
|
+
* - quale regola è stata applicata
|
|
500
|
+
* - le condizioni che hanno matchato
|
|
501
|
+
* - il risultato della formula
|
|
502
|
+
* - una spiegazione leggibile
|
|
503
|
+
*/
|
|
504
|
+
ruleApplications: defineTable({
|
|
505
|
+
runId: v.id("compensationRuns"),
|
|
506
|
+
/** Riga sorgente di produzione/storno che ha innescato il calcolo */
|
|
507
|
+
sourceLedgerEntryId: v.id("ledgerEntries"),
|
|
508
|
+
/** Riga compenso generata dal motore */
|
|
509
|
+
generatedLedgerEntryId: v.id("ledgerEntries"),
|
|
510
|
+
planId: v.id("commissionPlans"),
|
|
511
|
+
ruleId: v.id("commissionRules"),
|
|
512
|
+
/** Condizioni che hanno matchato (snapshot delle conditions della regola) */
|
|
513
|
+
matchedConditions: v.any(),
|
|
514
|
+
/** Snapshot della formula applicata */
|
|
515
|
+
formulaSnapshot: v.any(),
|
|
516
|
+
/** Input della formula: base di calcolo e parametri */
|
|
517
|
+
formulaInput: v.any(),
|
|
518
|
+
/** Output della formula: risultato e dettagli */
|
|
519
|
+
formulaOutput: v.any(),
|
|
520
|
+
resultAmount: v.number(),
|
|
521
|
+
/** Etichetta leggibile da analisi.md: "spiegazione delle regole applicate" */
|
|
522
|
+
explanationLabel: v.string(),
|
|
523
|
+
createdAt: v.number(),
|
|
524
|
+
})
|
|
525
|
+
.index("by_run", ["runId"])
|
|
526
|
+
.index("by_source_entry", ["sourceLedgerEntryId"])
|
|
527
|
+
.index("by_generated_entry", ["generatedLedgerEntryId"])
|
|
528
|
+
.index("by_run_rule", ["runId", "ruleId"]),
|
|
529
|
+
|
|
530
|
+
// ── ISSUES ─────────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Anomalie strutturate di un run — tracciamento interrogabile.
|
|
534
|
+
*
|
|
535
|
+
* A differenza degli auditLogs (immutabili, append-only),
|
|
536
|
+
* le runIssues vengono rigenerate ad ogni ciclo di verifica
|
|
537
|
+
* e rappresentano lo stato corrente delle anomalie del run.
|
|
538
|
+
*
|
|
539
|
+
* Tipi di anomalie:
|
|
540
|
+
* - no_active_plan: medico senza piano provvigionale attivo
|
|
541
|
+
* - no_matching_rule: riga produzione senza regola matchata
|
|
542
|
+
* - no_tax_profile: medico senza profilo fiscale valido
|
|
543
|
+
* - multiple_tax_profiles: ambiguità profilo fiscale
|
|
544
|
+
* - inactive_tax_profile: profilo fiscale non attivo
|
|
545
|
+
* - fiscal_anomaly: importi sospetti nella bozza fattura
|
|
546
|
+
* - missing_invoices: medici con commissioni ma senza bozza
|
|
547
|
+
*/
|
|
548
|
+
runIssues: defineTable({
|
|
549
|
+
runId: v.id("compensationRuns"),
|
|
550
|
+
issueType: issueType,
|
|
551
|
+
severity: issueSeverity,
|
|
552
|
+
userId: v.optional(v.string()),
|
|
553
|
+
clinicId: v.optional(v.string()),
|
|
554
|
+
message: v.string(),
|
|
555
|
+
details: v.optional(v.any()),
|
|
556
|
+
createdAt: v.number(),
|
|
557
|
+
})
|
|
558
|
+
.index("by_run", ["runId"])
|
|
559
|
+
.index("by_run_severity", ["runId", "severity"])
|
|
560
|
+
.index("by_run_type", ["runId", "issueType"]),
|
|
561
|
+
|
|
562
|
+
// ── PENDING COMPLETIONS ──────────────────────────────────
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Coda di eseguiti in attesa di elaborazione.
|
|
566
|
+
*
|
|
567
|
+
* Ogni volta che una prestazione di un PdC viene completata,
|
|
568
|
+
* PrimoUpCore registra qui l'evento. Un cron orario raccoglie
|
|
569
|
+
* tutti i pending e lancia il calcolo batch tramite il Python Worker.
|
|
570
|
+
*
|
|
571
|
+
* Dopo l'elaborazione, le righe vengono marcate come processed.
|
|
572
|
+
*/
|
|
573
|
+
pendingCompletions: defineTable({
|
|
574
|
+
carePlanRowId: v.string(),
|
|
575
|
+
carePlanId: v.string(),
|
|
576
|
+
userId: v.string(),
|
|
577
|
+
clinicId: v.string(),
|
|
578
|
+
companyId: v.optional(v.string()),
|
|
579
|
+
period: v.string(),
|
|
580
|
+
amount: v.number(),
|
|
581
|
+
serviceDescription: v.optional(v.string()),
|
|
582
|
+
completedAt: v.number(),
|
|
583
|
+
completedBy: v.string(),
|
|
584
|
+
isProcessed: v.boolean(),
|
|
585
|
+
processedAt: v.optional(v.number()),
|
|
586
|
+
processedRunId: v.optional(v.string()),
|
|
587
|
+
createdAt: v.number(),
|
|
588
|
+
})
|
|
589
|
+
.index("by_processed", ["isProcessed"])
|
|
590
|
+
.index("by_period_processed", ["period", "isProcessed"])
|
|
591
|
+
.index("by_cpr_id", ["carePlanRowId"]),
|
|
592
|
+
|
|
593
|
+
// ── AUDIT ──────────────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Log di audit — tracciamento completo delle operazioni.
|
|
597
|
+
* Da analisi.md: "audit base (chi, cosa, quando)" e "tutto spiegabile".
|
|
598
|
+
* Immutabili — una volta scritti non vengono mai modificati.
|
|
599
|
+
*/
|
|
600
|
+
auditLogs: defineTable({
|
|
601
|
+
entityType: v.string(),
|
|
602
|
+
entityId: v.string(),
|
|
603
|
+
action: v.string(),
|
|
604
|
+
userId: v.string(),
|
|
605
|
+
payload: v.optional(v.any()),
|
|
606
|
+
createdAt: v.number(),
|
|
607
|
+
})
|
|
608
|
+
.index("by_entity", ["entityType", "entityId"])
|
|
609
|
+
.index("by_user", ["userId"])
|
|
610
|
+
.index("by_action", ["action"]),
|
|
611
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mutation } from "./_generated/server";
|
|
2
|
+
|
|
3
|
+
const COMPONENT_TABLES = [
|
|
4
|
+
"pendingCompletions",
|
|
5
|
+
"runIssues",
|
|
6
|
+
"ruleApplications",
|
|
7
|
+
"draftInvoices",
|
|
8
|
+
"ledgerEntries",
|
|
9
|
+
"compensationRuns",
|
|
10
|
+
"commissionRules",
|
|
11
|
+
"commissionPlans",
|
|
12
|
+
"commissionPlanTemplates",
|
|
13
|
+
"userTaxProfiles",
|
|
14
|
+
"taxProfiles",
|
|
15
|
+
"fixedCommissionTypes",
|
|
16
|
+
"deductionRules",
|
|
17
|
+
"companyMappings",
|
|
18
|
+
"reimbursementCategories",
|
|
19
|
+
"adjustmentCategories",
|
|
20
|
+
"invisalignConfig",
|
|
21
|
+
"invisalignMinUsers",
|
|
22
|
+
"globalSettings",
|
|
23
|
+
"auditLogs",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export const clearAll = mutation({
|
|
27
|
+
args: {},
|
|
28
|
+
handler: async (ctx) => {
|
|
29
|
+
let totalDeleted = 0;
|
|
30
|
+
|
|
31
|
+
for (const table of COMPONENT_TABLES) {
|
|
32
|
+
const rows = await ctx.db.query(table).collect();
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
await ctx.db.delete(row._id);
|
|
35
|
+
totalDeleted++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { deleted: totalDeleted };
|
|
40
|
+
},
|
|
41
|
+
});
|