@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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "../_generated/server";
|
|
3
|
+
|
|
4
|
+
const conditionsValidator = v.object({
|
|
5
|
+
companyIds: v.optional(v.array(v.string())),
|
|
6
|
+
carePlanTypeIds: v.optional(v.array(v.number())),
|
|
7
|
+
isInsurance: v.optional(v.boolean()),
|
|
8
|
+
isSinistro: v.optional(v.boolean()),
|
|
9
|
+
percentageThreshold: v.optional(v.number()),
|
|
10
|
+
percentageAboveThreshold: v.optional(v.boolean()),
|
|
11
|
+
conventionNameContains: v.optional(v.string()),
|
|
12
|
+
serviceCategoryNames: v.optional(v.array(v.string())),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const create = mutation({
|
|
16
|
+
args: {
|
|
17
|
+
code: v.string(),
|
|
18
|
+
label: v.string(),
|
|
19
|
+
description: v.optional(v.string()),
|
|
20
|
+
conditions: conditionsValidator,
|
|
21
|
+
deductionPercent: v.number(),
|
|
22
|
+
priority: v.number(),
|
|
23
|
+
createdBy: v.string(),
|
|
24
|
+
},
|
|
25
|
+
handler: async (ctx, args) => {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const { createdBy, ...fields } = args;
|
|
28
|
+
|
|
29
|
+
const id = await ctx.db.insert("deductionRules", {
|
|
30
|
+
...fields,
|
|
31
|
+
isActive: true,
|
|
32
|
+
createdBy,
|
|
33
|
+
createdAt: now,
|
|
34
|
+
updatedAt: now,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await ctx.db.insert("auditLogs", {
|
|
38
|
+
entityType: "deductionRules",
|
|
39
|
+
entityId: id,
|
|
40
|
+
action: "created",
|
|
41
|
+
userId: createdBy,
|
|
42
|
+
payload: { code: args.code, label: args.label },
|
|
43
|
+
createdAt: now,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return id;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const update = mutation({
|
|
51
|
+
args: {
|
|
52
|
+
ruleId: v.id("deductionRules"),
|
|
53
|
+
label: v.optional(v.string()),
|
|
54
|
+
description: v.optional(v.string()),
|
|
55
|
+
conditions: v.optional(conditionsValidator),
|
|
56
|
+
deductionPercent: v.optional(v.number()),
|
|
57
|
+
priority: v.optional(v.number()),
|
|
58
|
+
isActive: v.optional(v.boolean()),
|
|
59
|
+
updatedBy: v.string(),
|
|
60
|
+
},
|
|
61
|
+
handler: async (ctx, args) => {
|
|
62
|
+
const { ruleId, updatedBy, ...fields } = args;
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
|
|
65
|
+
const patch: Record<string, unknown> = { updatedAt: now };
|
|
66
|
+
for (const [k, val] of Object.entries(fields)) {
|
|
67
|
+
if (val !== undefined) patch[k] = val;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await ctx.db.patch(ruleId, patch);
|
|
71
|
+
|
|
72
|
+
await ctx.db.insert("auditLogs", {
|
|
73
|
+
entityType: "deductionRules",
|
|
74
|
+
entityId: ruleId,
|
|
75
|
+
action: "updated",
|
|
76
|
+
userId: updatedBy,
|
|
77
|
+
payload: patch,
|
|
78
|
+
createdAt: now,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const get = query({
|
|
84
|
+
args: { ruleId: v.id("deductionRules") },
|
|
85
|
+
handler: async (ctx, args) => {
|
|
86
|
+
return await ctx.db.get(args.ruleId);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const listActive = query({
|
|
91
|
+
handler: async (ctx) => {
|
|
92
|
+
return await ctx.db
|
|
93
|
+
.query("deductionRules")
|
|
94
|
+
.withIndex("by_active_priority", (q) => q.eq("isActive", true))
|
|
95
|
+
.collect();
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const listAll = query({
|
|
100
|
+
handler: async (ctx) => {
|
|
101
|
+
return await ctx.db.query("deductionRules").collect();
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const getByCode = query({
|
|
106
|
+
args: { code: v.string() },
|
|
107
|
+
handler: async (ctx, args) => {
|
|
108
|
+
return await ctx.db
|
|
109
|
+
.query("deductionRules")
|
|
110
|
+
.withIndex("by_code", (q) => q.eq("code", args.code))
|
|
111
|
+
.first();
|
|
112
|
+
},
|
|
113
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "../_generated/server";
|
|
3
|
+
|
|
4
|
+
const behaviorValidator = v.union(
|
|
5
|
+
v.literal("minimum_guaranteed"),
|
|
6
|
+
v.literal("additive"),
|
|
7
|
+
v.literal("substitutive"),
|
|
8
|
+
v.literal("substitutive_no_wdays"),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export const create = mutation({
|
|
12
|
+
args: {
|
|
13
|
+
code: v.string(),
|
|
14
|
+
name: v.string(),
|
|
15
|
+
description: v.optional(v.string()),
|
|
16
|
+
behavior: behaviorValidator,
|
|
17
|
+
multipliedByWdays: v.boolean(),
|
|
18
|
+
},
|
|
19
|
+
handler: async (ctx, args) => {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
return await ctx.db.insert("fixedCommissionTypes", {
|
|
22
|
+
...args,
|
|
23
|
+
isActive: true,
|
|
24
|
+
createdAt: now,
|
|
25
|
+
updatedAt: now,
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const update = mutation({
|
|
31
|
+
args: {
|
|
32
|
+
typeId: v.id("fixedCommissionTypes"),
|
|
33
|
+
name: v.optional(v.string()),
|
|
34
|
+
description: v.optional(v.string()),
|
|
35
|
+
behavior: v.optional(behaviorValidator),
|
|
36
|
+
multipliedByWdays: v.optional(v.boolean()),
|
|
37
|
+
isActive: v.optional(v.boolean()),
|
|
38
|
+
},
|
|
39
|
+
handler: async (ctx, args) => {
|
|
40
|
+
const { typeId, ...fields } = args;
|
|
41
|
+
const patch: Record<string, unknown> = { updatedAt: Date.now() };
|
|
42
|
+
for (const [k, val] of Object.entries(fields)) {
|
|
43
|
+
if (val !== undefined) patch[k] = val;
|
|
44
|
+
}
|
|
45
|
+
await ctx.db.patch(typeId, patch);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const get = query({
|
|
50
|
+
args: { typeId: v.id("fixedCommissionTypes") },
|
|
51
|
+
handler: async (ctx, args) => {
|
|
52
|
+
return await ctx.db.get(args.typeId);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const listActive = query({
|
|
57
|
+
handler: async (ctx) => {
|
|
58
|
+
return await ctx.db
|
|
59
|
+
.query("fixedCommissionTypes")
|
|
60
|
+
.withIndex("by_active", (q) => q.eq("isActive", true))
|
|
61
|
+
.collect();
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const listAll = query({
|
|
66
|
+
handler: async (ctx) => {
|
|
67
|
+
return await ctx.db.query("fixedCommissionTypes").collect();
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const getByCode = query({
|
|
72
|
+
args: { code: v.string() },
|
|
73
|
+
handler: async (ctx, args) => {
|
|
74
|
+
return await ctx.db
|
|
75
|
+
.query("fixedCommissionTypes")
|
|
76
|
+
.withIndex("by_code", (q) => q.eq("code", args.code))
|
|
77
|
+
.first();
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "../_generated/server";
|
|
3
|
+
|
|
4
|
+
export const set = mutation({
|
|
5
|
+
args: {
|
|
6
|
+
key: v.string(),
|
|
7
|
+
value: v.string(),
|
|
8
|
+
description: v.optional(v.string()),
|
|
9
|
+
updatedBy: v.string(),
|
|
10
|
+
},
|
|
11
|
+
handler: async (ctx, args) => {
|
|
12
|
+
const existing = await ctx.db
|
|
13
|
+
.query("globalSettings")
|
|
14
|
+
.withIndex("by_key", (q) => q.eq("key", args.key))
|
|
15
|
+
.first();
|
|
16
|
+
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
|
|
19
|
+
if (existing) {
|
|
20
|
+
await ctx.db.patch(existing._id, {
|
|
21
|
+
value: args.value,
|
|
22
|
+
description: args.description ?? existing.description,
|
|
23
|
+
updatedBy: args.updatedBy,
|
|
24
|
+
updatedAt: now,
|
|
25
|
+
});
|
|
26
|
+
return existing._id;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return await ctx.db.insert("globalSettings", {
|
|
30
|
+
key: args.key,
|
|
31
|
+
value: args.value,
|
|
32
|
+
description: args.description,
|
|
33
|
+
updatedBy: args.updatedBy,
|
|
34
|
+
updatedAt: now,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const get = query({
|
|
40
|
+
args: { key: v.string() },
|
|
41
|
+
handler: async (ctx, args) => {
|
|
42
|
+
return await ctx.db
|
|
43
|
+
.query("globalSettings")
|
|
44
|
+
.withIndex("by_key", (q) => q.eq("key", args.key))
|
|
45
|
+
.first();
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const getNumber = query({
|
|
50
|
+
args: { key: v.string(), defaultValue: v.number() },
|
|
51
|
+
handler: async (ctx, args) => {
|
|
52
|
+
const setting = await ctx.db
|
|
53
|
+
.query("globalSettings")
|
|
54
|
+
.withIndex("by_key", (q) => q.eq("key", args.key))
|
|
55
|
+
.first();
|
|
56
|
+
if (!setting) return args.defaultValue;
|
|
57
|
+
const parsed = parseFloat(setting.value);
|
|
58
|
+
return isNaN(parsed) ? args.defaultValue : parsed;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const listAll = query({
|
|
63
|
+
handler: async (ctx) => {
|
|
64
|
+
return await ctx.db.query("globalSettings").collect();
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const remove = mutation({
|
|
69
|
+
args: { key: v.string() },
|
|
70
|
+
handler: async (ctx, args) => {
|
|
71
|
+
const existing = await ctx.db
|
|
72
|
+
.query("globalSettings")
|
|
73
|
+
.withIndex("by_key", (q) => q.eq("key", args.key))
|
|
74
|
+
.first();
|
|
75
|
+
if (existing) {
|
|
76
|
+
await ctx.db.delete(existing._id);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "../_generated/server";
|
|
3
|
+
|
|
4
|
+
export const upsert = mutation({
|
|
5
|
+
args: {
|
|
6
|
+
listRowId: v.string(),
|
|
7
|
+
firstPhasePercentage: v.number(),
|
|
8
|
+
otherPhasesPercentage: v.number(),
|
|
9
|
+
firstPhaseMin: v.number(),
|
|
10
|
+
otherPhasesMin: v.number(),
|
|
11
|
+
},
|
|
12
|
+
handler: async (ctx, args) => {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const existing = await ctx.db
|
|
15
|
+
.query("invisalignConfig")
|
|
16
|
+
.withIndex("by_listRowId", (q) => q.eq("listRowId", args.listRowId))
|
|
17
|
+
.first();
|
|
18
|
+
|
|
19
|
+
if (existing) {
|
|
20
|
+
await ctx.db.patch(existing._id, {
|
|
21
|
+
firstPhasePercentage: args.firstPhasePercentage,
|
|
22
|
+
otherPhasesPercentage: args.otherPhasesPercentage,
|
|
23
|
+
firstPhaseMin: args.firstPhaseMin,
|
|
24
|
+
otherPhasesMin: args.otherPhasesMin,
|
|
25
|
+
updatedAt: now,
|
|
26
|
+
});
|
|
27
|
+
return existing._id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return await ctx.db.insert("invisalignConfig", {
|
|
31
|
+
...args,
|
|
32
|
+
isActive: true,
|
|
33
|
+
createdAt: now,
|
|
34
|
+
updatedAt: now,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const listActive = query({
|
|
40
|
+
handler: async (ctx) => {
|
|
41
|
+
return await ctx.db
|
|
42
|
+
.query("invisalignConfig")
|
|
43
|
+
.withIndex("by_active", (q) => q.eq("isActive", true))
|
|
44
|
+
.collect();
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const listAll = query({
|
|
49
|
+
handler: async (ctx) => {
|
|
50
|
+
return await ctx.db.query("invisalignConfig").collect();
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const addMinUser = mutation({
|
|
55
|
+
args: { userId: v.string() },
|
|
56
|
+
handler: async (ctx, args) => {
|
|
57
|
+
const existing = await ctx.db
|
|
58
|
+
.query("invisalignMinUsers")
|
|
59
|
+
.withIndex("by_userId", (q) => q.eq("userId", args.userId))
|
|
60
|
+
.first();
|
|
61
|
+
if (existing) return existing._id;
|
|
62
|
+
return await ctx.db.insert("invisalignMinUsers", {
|
|
63
|
+
userId: args.userId,
|
|
64
|
+
isActive: true,
|
|
65
|
+
createdAt: Date.now(),
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const removeMinUser = mutation({
|
|
71
|
+
args: { userId: v.string() },
|
|
72
|
+
handler: async (ctx, args) => {
|
|
73
|
+
const existing = await ctx.db
|
|
74
|
+
.query("invisalignMinUsers")
|
|
75
|
+
.withIndex("by_userId", (q) => q.eq("userId", args.userId))
|
|
76
|
+
.first();
|
|
77
|
+
if (existing) {
|
|
78
|
+
await ctx.db.patch(existing._id, { isActive: false });
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const listMinUsers = query({
|
|
84
|
+
handler: async (ctx) => {
|
|
85
|
+
return await ctx.db.query("invisalignMinUsers").collect();
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "../_generated/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Template di piani provvigionali.
|
|
6
|
+
* Da analisi.md: i piani sono "basati su template" e "importabili da CSV/Excel".
|
|
7
|
+
*
|
|
8
|
+
* Un template contiene una struttura di regole predefinite
|
|
9
|
+
* che vengono copiate quando si crea un nuovo piano.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const create = mutation({
|
|
13
|
+
args: {
|
|
14
|
+
code: v.string(),
|
|
15
|
+
name: v.string(),
|
|
16
|
+
description: v.optional(v.string()),
|
|
17
|
+
defaultRules: v.optional(v.any()),
|
|
18
|
+
createdBy: v.string(),
|
|
19
|
+
},
|
|
20
|
+
handler: async (ctx, args) => {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const { createdBy, ...fields } = args;
|
|
23
|
+
|
|
24
|
+
const templateId = await ctx.db.insert("commissionPlanTemplates", {
|
|
25
|
+
...fields,
|
|
26
|
+
isActive: true,
|
|
27
|
+
createdBy,
|
|
28
|
+
createdAt: now,
|
|
29
|
+
updatedAt: now,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await ctx.db.insert("auditLogs", {
|
|
33
|
+
entityType: "commissionPlanTemplates",
|
|
34
|
+
entityId: templateId,
|
|
35
|
+
action: "created",
|
|
36
|
+
userId: createdBy,
|
|
37
|
+
payload: { code: args.code, name: args.name },
|
|
38
|
+
createdAt: now,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return templateId;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const update = mutation({
|
|
46
|
+
args: {
|
|
47
|
+
templateId: v.id("commissionPlanTemplates"),
|
|
48
|
+
name: v.optional(v.string()),
|
|
49
|
+
description: v.optional(v.string()),
|
|
50
|
+
defaultRules: v.optional(v.any()),
|
|
51
|
+
isActive: v.optional(v.boolean()),
|
|
52
|
+
updatedBy: v.string(),
|
|
53
|
+
},
|
|
54
|
+
handler: async (ctx, args) => {
|
|
55
|
+
const { templateId, updatedBy, ...fields } = args;
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
|
|
58
|
+
const patch: Record<string, unknown> = { updatedAt: now };
|
|
59
|
+
for (const [k, val] of Object.entries(fields)) {
|
|
60
|
+
if (val !== undefined) patch[k] = val;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await ctx.db.patch(templateId, patch);
|
|
64
|
+
|
|
65
|
+
await ctx.db.insert("auditLogs", {
|
|
66
|
+
entityType: "commissionPlanTemplates",
|
|
67
|
+
entityId: templateId,
|
|
68
|
+
action: "updated",
|
|
69
|
+
userId: updatedBy,
|
|
70
|
+
payload: patch,
|
|
71
|
+
createdAt: now,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const get = query({
|
|
77
|
+
args: { templateId: v.id("commissionPlanTemplates") },
|
|
78
|
+
handler: async (ctx, args) => {
|
|
79
|
+
return await ctx.db.get(args.templateId);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const listActive = query({
|
|
84
|
+
handler: async (ctx) => {
|
|
85
|
+
return await ctx.db
|
|
86
|
+
.query("commissionPlanTemplates")
|
|
87
|
+
.withIndex("by_active", (q) => q.eq("isActive", true))
|
|
88
|
+
.collect();
|
|
89
|
+
},
|
|
90
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "../_generated/server";
|
|
3
|
+
|
|
4
|
+
export const create = mutation({
|
|
5
|
+
args: {
|
|
6
|
+
code: v.string(),
|
|
7
|
+
name: v.string(),
|
|
8
|
+
description: v.optional(v.string()),
|
|
9
|
+
vatRatePercent: v.number(),
|
|
10
|
+
withholdingRatePercent: v.number(),
|
|
11
|
+
withholdingOnPercent: v.number(),
|
|
12
|
+
contributionRatePercent: v.number(),
|
|
13
|
+
contributionWithholding: v.boolean(),
|
|
14
|
+
contributionText: v.optional(v.string()),
|
|
15
|
+
stampDutyEnabled: v.boolean(),
|
|
16
|
+
stampDutyAmount: v.number(),
|
|
17
|
+
stampDutyThreshold: v.number(),
|
|
18
|
+
is104: v.boolean(),
|
|
19
|
+
isBolloCompreso: v.boolean(),
|
|
20
|
+
footerText: v.optional(v.string()),
|
|
21
|
+
additionalRules: v.optional(v.any()),
|
|
22
|
+
createdBy: v.string(),
|
|
23
|
+
},
|
|
24
|
+
handler: async (ctx, args) => {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const { createdBy, ...fields } = args;
|
|
27
|
+
|
|
28
|
+
const profileId = await ctx.db.insert("taxProfiles", {
|
|
29
|
+
...fields,
|
|
30
|
+
isActive: true,
|
|
31
|
+
createdBy,
|
|
32
|
+
createdAt: now,
|
|
33
|
+
updatedAt: now,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await ctx.db.insert("auditLogs", {
|
|
37
|
+
entityType: "taxProfiles",
|
|
38
|
+
entityId: profileId,
|
|
39
|
+
action: "created",
|
|
40
|
+
userId: createdBy,
|
|
41
|
+
payload: { code: args.code, name: args.name },
|
|
42
|
+
createdAt: now,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return profileId;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const update = mutation({
|
|
50
|
+
args: {
|
|
51
|
+
profileId: v.id("taxProfiles"),
|
|
52
|
+
name: v.optional(v.string()),
|
|
53
|
+
description: v.optional(v.string()),
|
|
54
|
+
vatRatePercent: v.optional(v.number()),
|
|
55
|
+
withholdingRatePercent: v.optional(v.number()),
|
|
56
|
+
withholdingOnPercent: v.optional(v.number()),
|
|
57
|
+
contributionRatePercent: v.optional(v.number()),
|
|
58
|
+
contributionWithholding: v.optional(v.boolean()),
|
|
59
|
+
contributionText: v.optional(v.string()),
|
|
60
|
+
stampDutyEnabled: v.optional(v.boolean()),
|
|
61
|
+
stampDutyAmount: v.optional(v.number()),
|
|
62
|
+
stampDutyThreshold: v.optional(v.number()),
|
|
63
|
+
is104: v.optional(v.boolean()),
|
|
64
|
+
isBolloCompreso: v.optional(v.boolean()),
|
|
65
|
+
footerText: v.optional(v.string()),
|
|
66
|
+
additionalRules: v.optional(v.any()),
|
|
67
|
+
isActive: v.optional(v.boolean()),
|
|
68
|
+
updatedBy: v.string(),
|
|
69
|
+
},
|
|
70
|
+
handler: async (ctx, args) => {
|
|
71
|
+
const { profileId, updatedBy, ...fields } = args;
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
|
|
74
|
+
const patch: Record<string, unknown> = { updatedAt: now };
|
|
75
|
+
for (const [k, val] of Object.entries(fields)) {
|
|
76
|
+
if (val !== undefined) patch[k] = val;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await ctx.db.patch(profileId, patch);
|
|
80
|
+
|
|
81
|
+
await ctx.db.insert("auditLogs", {
|
|
82
|
+
entityType: "taxProfiles",
|
|
83
|
+
entityId: profileId,
|
|
84
|
+
action: "updated",
|
|
85
|
+
userId: updatedBy,
|
|
86
|
+
payload: patch,
|
|
87
|
+
createdAt: now,
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const get = query({
|
|
93
|
+
args: { profileId: v.id("taxProfiles") },
|
|
94
|
+
handler: async (ctx, args) => {
|
|
95
|
+
return await ctx.db.get(args.profileId);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const listActive = query({
|
|
100
|
+
handler: async (ctx) => {
|
|
101
|
+
return await ctx.db
|
|
102
|
+
.query("taxProfiles")
|
|
103
|
+
.withIndex("by_active", (q) => q.eq("isActive", true))
|
|
104
|
+
.collect();
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const listAll = query({
|
|
109
|
+
handler: async (ctx) => {
|
|
110
|
+
return await ctx.db.query("taxProfiles").collect();
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const getByCode = query({
|
|
115
|
+
args: { code: v.string() },
|
|
116
|
+
handler: async (ctx, args) => {
|
|
117
|
+
return await ctx.db
|
|
118
|
+
.query("taxProfiles")
|
|
119
|
+
.withIndex("by_code", (q) => q.eq("code", args.code))
|
|
120
|
+
.first();
|
|
121
|
+
},
|
|
122
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "../_generated/server";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Assegnazione profilo fiscale a un medico nel tempo.
|
|
6
|
+
* Un medico può cambiare regime fiscale; questa tabella
|
|
7
|
+
* traccia quale profilo è valido in quale periodo.
|
|
8
|
+
*
|
|
9
|
+
* Separata da taxProfiles per supportare lo storico:
|
|
10
|
+
* se un medico passa da forfettario a ordinario,
|
|
11
|
+
* entrambe le assegnazioni restano nel sistema.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const assign = mutation({
|
|
15
|
+
args: {
|
|
16
|
+
userId: v.string(),
|
|
17
|
+
taxProfileId: v.id("taxProfiles"),
|
|
18
|
+
effectiveFrom: v.number(),
|
|
19
|
+
effectiveTo: v.optional(v.number()),
|
|
20
|
+
notes: v.optional(v.string()),
|
|
21
|
+
createdBy: v.string(),
|
|
22
|
+
},
|
|
23
|
+
handler: async (ctx, args) => {
|
|
24
|
+
const { createdBy, ...fields } = args;
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
|
|
27
|
+
const assignmentId = await ctx.db.insert("userTaxProfiles", {
|
|
28
|
+
...fields,
|
|
29
|
+
createdBy,
|
|
30
|
+
createdAt: now,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await ctx.db.insert("auditLogs", {
|
|
34
|
+
entityType: "userTaxProfiles",
|
|
35
|
+
entityId: assignmentId,
|
|
36
|
+
action: "assigned",
|
|
37
|
+
userId: createdBy,
|
|
38
|
+
payload: {
|
|
39
|
+
targetUserId: args.userId,
|
|
40
|
+
taxProfileId: args.taxProfileId,
|
|
41
|
+
},
|
|
42
|
+
createdAt: now,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return assignmentId;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Elenca tutte le assegnazioni per un medico (storico completo) */
|
|
50
|
+
export const listByUser = query({
|
|
51
|
+
args: { userId: v.string() },
|
|
52
|
+
handler: async (ctx, args) => {
|
|
53
|
+
return await ctx.db
|
|
54
|
+
.query("userTaxProfiles")
|
|
55
|
+
.withIndex("by_user", (q) => q.eq("userId", args.userId))
|
|
56
|
+
.collect();
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Trova il profilo fiscale effettivo per un medico a una data.
|
|
62
|
+
* Restituisce l'assegnazione con effectiveFrom ≤ date
|
|
63
|
+
* e effectiveTo > date (o senza scadenza).
|
|
64
|
+
*/
|
|
65
|
+
export const getEffective = query({
|
|
66
|
+
args: {
|
|
67
|
+
userId: v.string(),
|
|
68
|
+
atDate: v.number(),
|
|
69
|
+
},
|
|
70
|
+
handler: async (ctx, args) => {
|
|
71
|
+
const assignments = await ctx.db
|
|
72
|
+
.query("userTaxProfiles")
|
|
73
|
+
.withIndex("by_user_effective", (q) => q.eq("userId", args.userId))
|
|
74
|
+
.collect();
|
|
75
|
+
|
|
76
|
+
const effective = assignments.find(
|
|
77
|
+
(a) =>
|
|
78
|
+
a.effectiveFrom <= args.atDate &&
|
|
79
|
+
(a.effectiveTo === undefined || a.effectiveTo > args.atDate),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!effective) return null;
|
|
83
|
+
|
|
84
|
+
const profile = await ctx.db.get(effective.taxProfileId);
|
|
85
|
+
return { assignment: effective, profile };
|
|
86
|
+
},
|
|
87
|
+
});
|