@studious-lms/server 1.23.0 → 1.24.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.
Files changed (298) hide show
  1. package/.env.example +0 -1
  2. package/data/curriculum/nepal-cdc-g4-g5/README.md +90 -0
  3. package/data/curriculum/nepal-cdc-g4-g5/manifest.json +97 -0
  4. package/data/curriculum/nepal-cdc-g4-g5/proficiency-scale.json +76 -0
  5. package/data/curriculum/nepal-cdc-g4-g5/subjects/english.json +6936 -0
  6. package/data/curriculum/nepal-cdc-g4-g5/subjects/health-physical-creative-arts.json +3401 -0
  7. package/data/curriculum/nepal-cdc-g4-g5/subjects/mathematics.json +1511 -0
  8. package/data/curriculum/nepal-cdc-g4-g5/subjects/nepali.json +7928 -0
  9. package/data/curriculum/nepal-cdc-g4-g5/subjects/science-technology.json +2420 -0
  10. package/data/curriculum/nepal-cdc-g4-g5/subjects/social-studies.json +2177 -0
  11. package/dist/lib/assignmentTypeDefaults.d.ts +41 -0
  12. package/dist/lib/assignmentTypeDefaults.d.ts.map +1 -0
  13. package/dist/lib/assignmentTypeDefaults.js +167 -0
  14. package/dist/lib/assignmentTypeDefaults.js.map +1 -0
  15. package/dist/lib/config/env.d.ts +0 -8
  16. package/dist/lib/config/env.d.ts.map +1 -1
  17. package/dist/lib/config/env.js +2 -5
  18. package/dist/lib/config/env.js.map +1 -1
  19. package/dist/lib/feePdf.d.ts +19 -0
  20. package/dist/lib/feePdf.d.ts.map +1 -0
  21. package/dist/lib/feePdf.js +167 -0
  22. package/dist/lib/feePdf.js.map +1 -0
  23. package/dist/lib/feePdfFormat.d.ts +4 -0
  24. package/dist/lib/feePdfFormat.d.ts.map +1 -0
  25. package/dist/lib/feePdfFormat.js +16 -0
  26. package/dist/lib/feePdfFormat.js.map +1 -0
  27. package/dist/lib/idCardPdf.d.ts +26 -0
  28. package/dist/lib/idCardPdf.d.ts.map +1 -0
  29. package/dist/lib/idCardPdf.js +272 -0
  30. package/dist/lib/idCardPdf.js.map +1 -0
  31. package/dist/lib/pdfLetterhead.d.ts +41 -0
  32. package/dist/lib/pdfLetterhead.d.ts.map +1 -0
  33. package/dist/lib/pdfLetterhead.js +86 -0
  34. package/dist/lib/pdfLetterhead.js.map +1 -0
  35. package/dist/lib/transcriptPdf.d.ts +55 -0
  36. package/dist/lib/transcriptPdf.d.ts.map +1 -0
  37. package/dist/lib/transcriptPdf.js +303 -0
  38. package/dist/lib/transcriptPdf.js.map +1 -0
  39. package/dist/models/academicCalendar.d.ts +198 -0
  40. package/dist/models/academicCalendar.d.ts.map +1 -0
  41. package/dist/models/academicCalendar.js +106 -0
  42. package/dist/models/academicCalendar.js.map +1 -0
  43. package/dist/models/agenda.d.ts +8 -0
  44. package/dist/models/agenda.d.ts.map +1 -1
  45. package/dist/models/announcement.d.ts +2 -0
  46. package/dist/models/announcement.d.ts.map +1 -1
  47. package/dist/models/assignment.d.ts +66 -0
  48. package/dist/models/assignment.d.ts.map +1 -1
  49. package/dist/models/attendance.d.ts +659 -21
  50. package/dist/models/attendance.d.ts.map +1 -1
  51. package/dist/models/attendance.js +544 -75
  52. package/dist/models/attendance.js.map +1 -1
  53. package/dist/models/auth.d.ts +2 -0
  54. package/dist/models/auth.d.ts.map +1 -1
  55. package/dist/models/class.d.ts +12 -0
  56. package/dist/models/class.d.ts.map +1 -1
  57. package/dist/models/class.js +3 -2
  58. package/dist/models/class.js.map +1 -1
  59. package/dist/models/event.d.ts +16 -0
  60. package/dist/models/event.d.ts.map +1 -1
  61. package/dist/models/fees.d.ts +554 -0
  62. package/dist/models/fees.d.ts.map +1 -0
  63. package/dist/models/fees.js +278 -0
  64. package/dist/models/fees.js.map +1 -0
  65. package/dist/models/file.d.ts +29 -0
  66. package/dist/models/file.d.ts.map +1 -1
  67. package/dist/models/folder.d.ts +2 -0
  68. package/dist/models/folder.d.ts.map +1 -1
  69. package/dist/models/labChat.d.ts +2 -0
  70. package/dist/models/labChat.d.ts.map +1 -1
  71. package/dist/models/marketplace.d.ts +178 -0
  72. package/dist/models/marketplace.d.ts.map +1 -0
  73. package/dist/models/marketplace.js +115 -0
  74. package/dist/models/marketplace.js.map +1 -0
  75. package/dist/models/moduleItem.d.ts +304 -0
  76. package/dist/models/moduleItem.d.ts.map +1 -0
  77. package/dist/models/moduleItem.js +78 -0
  78. package/dist/models/moduleItem.js.map +1 -0
  79. package/dist/models/newtonChat.d.ts +1 -0
  80. package/dist/models/newtonChat.d.ts.map +1 -1
  81. package/dist/models/school.d.ts +18 -0
  82. package/dist/models/school.d.ts.map +1 -1
  83. package/dist/models/school.js +7 -2
  84. package/dist/models/school.js.map +1 -1
  85. package/dist/models/section.d.ts +4 -0
  86. package/dist/models/section.d.ts.map +1 -1
  87. package/dist/models/worksheet.d.ts +19 -0
  88. package/dist/models/worksheet.d.ts.map +1 -1
  89. package/dist/models/worksheet.js +2 -2
  90. package/dist/models/worksheet.js.map +1 -1
  91. package/dist/routers/_app.d.ts +11341 -7097
  92. package/dist/routers/_app.d.ts.map +1 -1
  93. package/dist/routers/_app.js +12 -2
  94. package/dist/routers/_app.js.map +1 -1
  95. package/dist/routers/academicCalendar.d.ts +229 -0
  96. package/dist/routers/academicCalendar.d.ts.map +1 -0
  97. package/dist/routers/academicCalendar.js +75 -0
  98. package/dist/routers/academicCalendar.js.map +1 -0
  99. package/dist/routers/admin.d.ts +2 -0
  100. package/dist/routers/admin.d.ts.map +1 -1
  101. package/dist/routers/agenda.d.ts +8 -0
  102. package/dist/routers/agenda.d.ts.map +1 -1
  103. package/dist/routers/assignment.d.ts +75 -0
  104. package/dist/routers/assignment.d.ts.map +1 -1
  105. package/dist/routers/assignment.js +10 -2
  106. package/dist/routers/assignment.js.map +1 -1
  107. package/dist/routers/attendance.d.ts +103 -0
  108. package/dist/routers/attendance.d.ts.map +1 -1
  109. package/dist/routers/attendance.js +8 -5
  110. package/dist/routers/attendance.js.map +1 -1
  111. package/dist/routers/class.d.ts +27 -0
  112. package/dist/routers/class.d.ts.map +1 -1
  113. package/dist/routers/class.js +6 -2
  114. package/dist/routers/class.js.map +1 -1
  115. package/dist/routers/event.d.ts +8 -0
  116. package/dist/routers/event.d.ts.map +1 -1
  117. package/dist/routers/fees.d.ts +513 -0
  118. package/dist/routers/fees.d.ts.map +1 -0
  119. package/dist/routers/fees.js +88 -0
  120. package/dist/routers/fees.js.map +1 -0
  121. package/dist/routers/file.d.ts +2 -0
  122. package/dist/routers/file.d.ts.map +1 -1
  123. package/dist/routers/gradebook.d.ts +4 -1
  124. package/dist/routers/gradebook.d.ts.map +1 -1
  125. package/dist/routers/marketplace.d.ts +371 -0
  126. package/dist/routers/marketplace.d.ts.map +1 -0
  127. package/dist/routers/marketplace.js +87 -0
  128. package/dist/routers/marketplace.js.map +1 -0
  129. package/dist/routers/moduleItem.d.ts +263 -0
  130. package/dist/routers/moduleItem.d.ts.map +1 -0
  131. package/dist/routers/moduleItem.js +60 -0
  132. package/dist/routers/moduleItem.js.map +1 -0
  133. package/dist/routers/parent.d.ts +169 -0
  134. package/dist/routers/parent.d.ts.map +1 -0
  135. package/dist/routers/parent.js +49 -0
  136. package/dist/routers/parent.js.map +1 -0
  137. package/dist/routers/school.d.ts +363 -7
  138. package/dist/routers/school.d.ts.map +1 -1
  139. package/dist/routers/school.js +100 -3
  140. package/dist/routers/school.js.map +1 -1
  141. package/dist/routers/worksheet.d.ts +27 -0
  142. package/dist/routers/worksheet.d.ts.map +1 -1
  143. package/dist/routers/worksheet.js +10 -3
  144. package/dist/routers/worksheet.js.map +1 -1
  145. package/dist/services/academicCalendar.d.ts +173 -0
  146. package/dist/services/academicCalendar.d.ts.map +1 -0
  147. package/dist/services/academicCalendar.js +214 -0
  148. package/dist/services/academicCalendar.js.map +1 -0
  149. package/dist/services/adminInstitution.d.ts +2 -0
  150. package/dist/services/adminInstitution.d.ts.map +1 -1
  151. package/dist/services/agenda.d.ts +8 -0
  152. package/dist/services/agenda.d.ts.map +1 -1
  153. package/dist/services/assignment.d.ts +75 -0
  154. package/dist/services/assignment.d.ts.map +1 -1
  155. package/dist/services/assignment.js +91 -12
  156. package/dist/services/assignment.js.map +1 -1
  157. package/dist/services/attendance.d.ts +323 -10
  158. package/dist/services/attendance.d.ts.map +1 -1
  159. package/dist/services/attendance.js +385 -25
  160. package/dist/services/attendance.js.map +1 -1
  161. package/dist/services/attendanceMerge.d.ts +53 -0
  162. package/dist/services/attendanceMerge.d.ts.map +1 -0
  163. package/dist/services/attendanceMerge.js +87 -0
  164. package/dist/services/attendanceMerge.js.map +1 -0
  165. package/dist/services/attendanceRate.d.ts +24 -0
  166. package/dist/services/attendanceRate.d.ts.map +1 -0
  167. package/dist/services/attendanceRate.js +31 -0
  168. package/dist/services/attendanceRate.js.map +1 -0
  169. package/dist/services/attendanceStats.d.ts +22 -0
  170. package/dist/services/attendanceStats.d.ts.map +1 -0
  171. package/dist/services/attendanceStats.js +45 -0
  172. package/dist/services/attendanceStats.js.map +1 -0
  173. package/dist/services/class.d.ts +27 -0
  174. package/dist/services/class.d.ts.map +1 -1
  175. package/dist/services/class.js +26 -3
  176. package/dist/services/class.js.map +1 -1
  177. package/dist/services/event.d.ts +8 -0
  178. package/dist/services/event.d.ts.map +1 -1
  179. package/dist/services/fees.d.ts +422 -0
  180. package/dist/services/fees.d.ts.map +1 -0
  181. package/dist/services/fees.js +310 -0
  182. package/dist/services/fees.js.map +1 -0
  183. package/dist/services/feesAuthz.d.ts +49 -0
  184. package/dist/services/feesAuthz.d.ts.map +1 -0
  185. package/dist/services/feesAuthz.js +52 -0
  186. package/dist/services/feesAuthz.js.map +1 -0
  187. package/dist/services/file.d.ts +4 -0
  188. package/dist/services/file.d.ts.map +1 -1
  189. package/dist/services/gradebook.d.ts +18 -1
  190. package/dist/services/gradebook.d.ts.map +1 -1
  191. package/dist/services/gradebook.js +57 -2
  192. package/dist/services/gradebook.js.map +1 -1
  193. package/dist/services/idCard.d.ts +11 -0
  194. package/dist/services/idCard.d.ts.map +1 -0
  195. package/dist/services/idCard.js +122 -0
  196. package/dist/services/idCard.js.map +1 -0
  197. package/dist/services/marketplace.d.ts +301 -0
  198. package/dist/services/marketplace.d.ts.map +1 -0
  199. package/dist/services/marketplace.js +628 -0
  200. package/dist/services/marketplace.js.map +1 -0
  201. package/dist/services/moduleItem.d.ts +196 -0
  202. package/dist/services/moduleItem.d.ts.map +1 -0
  203. package/dist/services/moduleItem.js +188 -0
  204. package/dist/services/moduleItem.js.map +1 -0
  205. package/dist/services/notification.d.ts +58 -0
  206. package/dist/services/notification.d.ts.map +1 -1
  207. package/dist/services/notification.js +30 -2
  208. package/dist/services/notification.js.map +1 -1
  209. package/dist/services/parentPortal.d.ts +133 -0
  210. package/dist/services/parentPortal.d.ts.map +1 -0
  211. package/dist/services/parentPortal.js +328 -0
  212. package/dist/services/parentPortal.js.map +1 -0
  213. package/dist/services/school.d.ts +28 -3
  214. package/dist/services/school.d.ts.map +1 -1
  215. package/dist/services/school.js +65 -45
  216. package/dist/services/school.js.map +1 -1
  217. package/dist/services/studentImport.d.ts +34 -0
  218. package/dist/services/studentImport.d.ts.map +1 -0
  219. package/dist/services/studentImport.js +236 -0
  220. package/dist/services/studentImport.js.map +1 -0
  221. package/dist/services/transcript.d.ts +61 -0
  222. package/dist/services/transcript.d.ts.map +1 -0
  223. package/dist/services/transcript.js +243 -0
  224. package/dist/services/transcript.js.map +1 -0
  225. package/dist/services/worksheet.d.ts +22 -0
  226. package/dist/services/worksheet.d.ts.map +1 -1
  227. package/dist/services/worksheet.js +94 -2
  228. package/dist/services/worksheet.js.map +1 -1
  229. package/dist/trpc.d.ts +1 -1
  230. package/dist/trpc.d.ts.map +1 -1
  231. package/dist/trpc.js +5 -5
  232. package/dist/trpc.js.map +1 -1
  233. package/dist/utils/adminPanelAccess.d.ts +4 -5
  234. package/dist/utils/adminPanelAccess.d.ts.map +1 -1
  235. package/dist/utils/adminPanelAccess.js +12 -34
  236. package/dist/utils/adminPanelAccess.js.map +1 -1
  237. package/package.json +2 -1
  238. package/prisma/migrations/20260804120000_add_attendance_records_and_excusals/migration.sql +114 -0
  239. package/prisma/migrations/20260804120100_add_fee_plans_invoices_payments/migration.sql +101 -0
  240. package/prisma/migrations/20260804120200_add_marketplace_listings/migration.sql +54 -0
  241. package/prisma/migrations/20260804120300_content_platform_b3_b9/migration.sql +63 -0
  242. package/prisma/migrations/20260804120400_c_academic_ops/migration.sql +77 -0
  243. package/prisma/schema.prisma +371 -11
  244. package/scripts/import-nepal-cdc-curriculum.ts +379 -0
  245. package/src/lib/assignmentTypeDefaults.ts +222 -0
  246. package/src/lib/config/env.ts +0 -3
  247. package/src/lib/feePdf.ts +199 -0
  248. package/src/lib/feePdfFormat.ts +14 -0
  249. package/src/lib/idCardPdf.ts +336 -0
  250. package/src/lib/pdfLetterhead.ts +124 -0
  251. package/src/lib/transcriptPdf.ts +502 -0
  252. package/src/models/academicCalendar.ts +139 -0
  253. package/src/models/attendance.ts +704 -73
  254. package/src/models/class.ts +1 -0
  255. package/src/models/fees.ts +349 -0
  256. package/src/models/marketplace.ts +158 -0
  257. package/src/models/moduleItem.ts +107 -0
  258. package/src/models/school.ts +6 -0
  259. package/src/models/worksheet.ts +2 -0
  260. package/src/routers/_app.ts +10 -0
  261. package/src/routers/academicCalendar.ts +103 -0
  262. package/src/routers/assignment.ts +8 -0
  263. package/src/routers/attendance.ts +7 -3
  264. package/src/routers/class.ts +4 -0
  265. package/src/routers/fees.ts +126 -0
  266. package/src/routers/marketplace.ts +134 -0
  267. package/src/routers/moduleItem.ts +89 -0
  268. package/src/routers/parent.ts +68 -0
  269. package/src/routers/school.ts +139 -1
  270. package/src/routers/worksheet.ts +11 -0
  271. package/src/services/academicCalendar.ts +299 -0
  272. package/src/services/assignment.ts +122 -12
  273. package/src/services/attendance.ts +548 -26
  274. package/src/services/attendanceMerge.ts +139 -0
  275. package/src/services/attendanceRate.ts +41 -0
  276. package/src/services/attendanceStats.ts +63 -0
  277. package/src/services/class.ts +25 -1
  278. package/src/services/fees.ts +424 -0
  279. package/src/services/feesAuthz.ts +77 -0
  280. package/src/services/gradebook.ts +70 -0
  281. package/src/services/idCard.ts +147 -0
  282. package/src/services/marketplace.ts +785 -0
  283. package/src/services/moduleItem.ts +274 -0
  284. package/src/services/notification.ts +43 -0
  285. package/src/services/parentPortal.ts +380 -0
  286. package/src/services/school.ts +65 -44
  287. package/src/services/studentImport.ts +273 -0
  288. package/src/services/transcript.ts +313 -0
  289. package/src/services/worksheet.ts +111 -0
  290. package/src/trpc.ts +3 -3
  291. package/src/utils/adminPanelAccess.ts +12 -35
  292. package/tests/unit/assignmentTypeDefaults.test.ts +52 -0
  293. package/tests/unit/lib/feePdf.test.ts +15 -0
  294. package/tests/unit/lib/idCardPdf.test.ts +40 -0
  295. package/tests/unit/lib/transcriptPdf.test.ts +111 -0
  296. package/tests/unit/services/attendanceMerge.test.ts +90 -0
  297. package/tests/unit/services/attendanceRate.test.ts +38 -0
  298. package/tests/unit/services/attendanceStats.test.ts +54 -0
@@ -190,6 +190,7 @@ const classIncludeForGet = {
190
190
  discussionsEnabled: true,
191
191
  studentsCanCreateDiscussions: true,
192
192
  studentsCanReply: true,
193
+ sidebarFeatures: true,
193
194
  } as const;
194
195
 
195
196
  const assignmentSelect = {
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Fee ledger model — FeePlan, FeeInvoice, FeePayment queries.
3
+ */
4
+ import {
5
+ FeeInvoiceStatus,
6
+ FeePaymentMethod,
7
+ Prisma,
8
+ } from "@prisma/client";
9
+ import { prisma } from "../lib/prisma.js";
10
+
11
+ const studentSelect = {
12
+ id: true,
13
+ username: true,
14
+ email: true,
15
+ profile: {
16
+ select: {
17
+ displayName: true,
18
+ profilePicture: true,
19
+ profilePictureThumbnail: true,
20
+ },
21
+ },
22
+ } as const;
23
+
24
+ const planSelect = {
25
+ id: true,
26
+ schoolId: true,
27
+ name: true,
28
+ description: true,
29
+ amountNpr: true,
30
+ currency: true,
31
+ isActive: true,
32
+ createdAt: true,
33
+ updatedAt: true,
34
+ _count: { select: { enrollments: true, invoices: true } },
35
+ } as const;
36
+
37
+ const invoiceSelect = {
38
+ id: true,
39
+ schoolId: true,
40
+ feePlanId: true,
41
+ studentId: true,
42
+ year: true,
43
+ month: true,
44
+ amountNpr: true,
45
+ currency: true,
46
+ status: true,
47
+ dueDate: true,
48
+ issuedAt: true,
49
+ notes: true,
50
+ createdAt: true,
51
+ updatedAt: true,
52
+ feePlan: { select: { id: true, name: true } },
53
+ student: { select: studentSelect },
54
+ payments: {
55
+ orderBy: { paidAt: "desc" as const },
56
+ select: {
57
+ id: true,
58
+ amountNpr: true,
59
+ method: true,
60
+ reference: true,
61
+ paidAt: true,
62
+ notes: true,
63
+ recordedById: true,
64
+ },
65
+ },
66
+ } as const;
67
+
68
+ export function listFeePlans(schoolId: string) {
69
+ return prisma.feePlan.findMany({
70
+ where: { schoolId },
71
+ orderBy: [{ isActive: "desc" }, { name: "asc" }],
72
+ select: planSelect,
73
+ });
74
+ }
75
+
76
+ export function getFeePlan(planId: string) {
77
+ return prisma.feePlan.findUnique({
78
+ where: { id: planId },
79
+ select: {
80
+ ...planSelect,
81
+ enrollments: {
82
+ select: {
83
+ id: true,
84
+ studentId: true,
85
+ student: { select: studentSelect },
86
+ },
87
+ },
88
+ },
89
+ });
90
+ }
91
+
92
+ export function createFeePlan(data: {
93
+ schoolId: string;
94
+ name: string;
95
+ description?: string | null;
96
+ amountNpr: number;
97
+ currency?: string;
98
+ isActive?: boolean;
99
+ }) {
100
+ return prisma.feePlan.create({
101
+ data: {
102
+ schoolId: data.schoolId,
103
+ name: data.name,
104
+ description: data.description,
105
+ amountNpr: data.amountNpr,
106
+ currency: data.currency ?? "NPR",
107
+ isActive: data.isActive ?? true,
108
+ },
109
+ select: planSelect,
110
+ });
111
+ }
112
+
113
+ export function updateFeePlan(
114
+ planId: string,
115
+ data: {
116
+ name?: string;
117
+ description?: string | null;
118
+ amountNpr?: number;
119
+ isActive?: boolean;
120
+ }
121
+ ) {
122
+ return prisma.feePlan.update({
123
+ where: { id: planId },
124
+ data,
125
+ select: planSelect,
126
+ });
127
+ }
128
+
129
+ export function setFeePlanEnrollments(planId: string, studentIds: string[]) {
130
+ return prisma.$transaction(async (tx) => {
131
+ await tx.feePlanEnrollment.deleteMany({ where: { feePlanId: planId } });
132
+ if (studentIds.length) {
133
+ await tx.feePlanEnrollment.createMany({
134
+ data: studentIds.map((studentId) => ({ feePlanId: planId, studentId })),
135
+ skipDuplicates: true,
136
+ });
137
+ }
138
+ return tx.feePlan.findUnique({
139
+ where: { id: planId },
140
+ select: {
141
+ ...planSelect,
142
+ enrollments: {
143
+ select: {
144
+ id: true,
145
+ studentId: true,
146
+ student: { select: studentSelect },
147
+ },
148
+ },
149
+ },
150
+ });
151
+ });
152
+ }
153
+
154
+ export function listActiveStudentIds(schoolId: string) {
155
+ return prisma.schoolMembership
156
+ .findMany({
157
+ where: { schoolId, role: "STUDENT", status: "ACTIVE" },
158
+ select: { userId: true },
159
+ })
160
+ .then((rows) => rows.map((r) => r.userId));
161
+ }
162
+
163
+ export function listPlanEnrollmentIds(planId: string) {
164
+ return prisma.feePlanEnrollment
165
+ .findMany({
166
+ where: { feePlanId: planId },
167
+ select: { studentId: true },
168
+ })
169
+ .then((rows) => rows.map((r) => r.studentId));
170
+ }
171
+
172
+ export function createFeeInvoices(
173
+ rows: {
174
+ schoolId: string;
175
+ feePlanId: string;
176
+ studentId: string;
177
+ year: number;
178
+ month: number;
179
+ amountNpr: number;
180
+ currency: string;
181
+ dueDate?: Date | null;
182
+ }[]
183
+ ) {
184
+ return prisma.feeInvoice.createMany({
185
+ data: rows,
186
+ skipDuplicates: true,
187
+ });
188
+ }
189
+
190
+ export function listFeeInvoices(filters: {
191
+ schoolId: string;
192
+ year?: number;
193
+ month?: number;
194
+ studentId?: string;
195
+ feePlanId?: string;
196
+ status?: FeeInvoiceStatus;
197
+ }) {
198
+ const where: Prisma.FeeInvoiceWhereInput = {
199
+ schoolId: filters.schoolId,
200
+ };
201
+ if (filters.year != null) where.year = filters.year;
202
+ if (filters.month != null) where.month = filters.month;
203
+ if (filters.studentId) where.studentId = filters.studentId;
204
+ if (filters.feePlanId) where.feePlanId = filters.feePlanId;
205
+ if (filters.status) where.status = filters.status;
206
+
207
+ return prisma.feeInvoice.findMany({
208
+ where,
209
+ orderBy: [{ year: "desc" }, { month: "desc" }, { issuedAt: "desc" }],
210
+ select: invoiceSelect,
211
+ });
212
+ }
213
+
214
+ export function getFeeInvoice(invoiceId: string) {
215
+ return prisma.feeInvoice.findUnique({
216
+ where: { id: invoiceId },
217
+ select: {
218
+ ...invoiceSelect,
219
+ school: {
220
+ select: {
221
+ id: true,
222
+ name: true,
223
+ letterheadFileId: true,
224
+ letterhead: {
225
+ select: { id: true, path: true, type: true, name: true },
226
+ },
227
+ },
228
+ },
229
+ },
230
+ });
231
+ }
232
+
233
+ export function createFeePayment(data: {
234
+ schoolId: string;
235
+ invoiceId: string;
236
+ amountNpr: number;
237
+ method: FeePaymentMethod;
238
+ reference?: string | null;
239
+ paidAt?: Date;
240
+ recordedById: string;
241
+ notes?: string | null;
242
+ }) {
243
+ return prisma.feePayment.create({
244
+ data: {
245
+ schoolId: data.schoolId,
246
+ invoiceId: data.invoiceId,
247
+ amountNpr: data.amountNpr,
248
+ method: data.method,
249
+ reference: data.reference,
250
+ paidAt: data.paidAt,
251
+ recordedById: data.recordedById,
252
+ notes: data.notes,
253
+ },
254
+ });
255
+ }
256
+
257
+ export function sumPaymentsForInvoice(invoiceId: string) {
258
+ return prisma.feePayment.aggregate({
259
+ where: { invoiceId },
260
+ _sum: { amountNpr: true },
261
+ });
262
+ }
263
+
264
+ export function updateInvoiceStatus(invoiceId: string, status: FeeInvoiceStatus) {
265
+ return prisma.feeInvoice.update({
266
+ where: { id: invoiceId },
267
+ data: { status },
268
+ });
269
+ }
270
+
271
+ export function getFeePayment(paymentId: string) {
272
+ return prisma.feePayment.findUnique({
273
+ where: { id: paymentId },
274
+ include: {
275
+ invoice: {
276
+ select: {
277
+ ...invoiceSelect,
278
+ school: {
279
+ select: {
280
+ id: true,
281
+ name: true,
282
+ letterheadFileId: true,
283
+ letterhead: {
284
+ select: { id: true, path: true, type: true, name: true },
285
+ },
286
+ },
287
+ },
288
+ },
289
+ },
290
+ recordedBy: {
291
+ select: {
292
+ id: true,
293
+ username: true,
294
+ profile: { select: { displayName: true } },
295
+ },
296
+ },
297
+ },
298
+ });
299
+ }
300
+
301
+ export async function listStudentBalances(schoolId: string) {
302
+ const invoices = await prisma.feeInvoice.findMany({
303
+ where: {
304
+ schoolId,
305
+ status: { in: ["UNPAID", "PARTIAL"] },
306
+ },
307
+ select: {
308
+ studentId: true,
309
+ amountNpr: true,
310
+ student: { select: studentSelect },
311
+ payments: { select: { amountNpr: true } },
312
+ },
313
+ });
314
+
315
+ const byStudent = new Map<
316
+ string,
317
+ {
318
+ student: (typeof invoices)[number]["student"];
319
+ invoicedNpr: number;
320
+ paidNpr: number;
321
+ balanceNpr: number;
322
+ openInvoiceCount: number;
323
+ }
324
+ >();
325
+
326
+ for (const inv of invoices) {
327
+ const paid = inv.payments.reduce((s, p) => s + p.amountNpr, 0);
328
+ const balance = Math.max(0, inv.amountNpr - paid);
329
+ const existing = byStudent.get(inv.studentId);
330
+ if (existing) {
331
+ existing.invoicedNpr += inv.amountNpr;
332
+ existing.paidNpr += paid;
333
+ existing.balanceNpr += balance;
334
+ existing.openInvoiceCount += 1;
335
+ } else {
336
+ byStudent.set(inv.studentId, {
337
+ student: inv.student,
338
+ invoicedNpr: inv.amountNpr,
339
+ paidNpr: paid,
340
+ balanceNpr: balance,
341
+ openInvoiceCount: 1,
342
+ });
343
+ }
344
+ }
345
+
346
+ return Array.from(byStudent.entries())
347
+ .map(([studentId, row]) => ({ studentId, ...row }))
348
+ .sort((a, b) => b.balanceNpr - a.balanceNpr);
349
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * MarketplaceListing data access — B1 v1 (markschemes, worksheets, files).
3
+ * Isolated Marketplace* tables only; no Attendance/Fee coupling.
4
+ */
5
+ import type {
6
+ MarketplaceListingType,
7
+ MarketplaceVisibility,
8
+ Prisma,
9
+ } from "@prisma/client";
10
+ import { prisma } from "../lib/prisma.js";
11
+
12
+ export const marketplaceListingInclude = {
13
+ author: {
14
+ select: {
15
+ id: true,
16
+ username: true,
17
+ profile: { select: { displayName: true } },
18
+ },
19
+ },
20
+ school: { select: { id: true, name: true } },
21
+ } as const;
22
+
23
+ export type MarketplaceListingWithRelations = Prisma.MarketplaceListingGetPayload<{
24
+ include: typeof marketplaceListingInclude;
25
+ }>;
26
+
27
+ export async function createMarketplaceListing(data: {
28
+ type: MarketplaceListingType;
29
+ title: string;
30
+ description?: string | null;
31
+ authorId: string;
32
+ schoolId?: string | null;
33
+ visibility: MarketplaceVisibility;
34
+ markSchemeId?: string | null;
35
+ worksheetId?: string | null;
36
+ fileId?: string | null;
37
+ payload: Prisma.InputJsonValue;
38
+ tags?: string[];
39
+ }) {
40
+ return prisma.marketplaceListing.create({
41
+ data: {
42
+ type: data.type,
43
+ title: data.title,
44
+ description: data.description ?? null,
45
+ authorId: data.authorId,
46
+ schoolId: data.schoolId ?? null,
47
+ visibility: data.visibility,
48
+ markSchemeId: data.markSchemeId ?? null,
49
+ worksheetId: data.worksheetId ?? null,
50
+ fileId: data.fileId ?? null,
51
+ payload: data.payload,
52
+ tags: data.tags ?? [],
53
+ },
54
+ include: marketplaceListingInclude,
55
+ });
56
+ }
57
+
58
+ export async function findMarketplaceListingById(id: string) {
59
+ return prisma.marketplaceListing.findUnique({
60
+ where: { id },
61
+ include: marketplaceListingInclude,
62
+ });
63
+ }
64
+
65
+ export async function searchMarketplaceListings(input: {
66
+ schoolIds: string[];
67
+ type?: MarketplaceListingType;
68
+ search?: string;
69
+ tags?: string[];
70
+ cursor?: string;
71
+ limit: number;
72
+ /** When true, include the caller's own hidden listings (for authors). */
73
+ authorId?: string;
74
+ }) {
75
+ const search = input.search?.trim();
76
+ const where: Prisma.MarketplaceListingWhereInput = {
77
+ AND: [
78
+ {
79
+ OR: [
80
+ { visibility: "PUBLIC", isHidden: false },
81
+ {
82
+ visibility: "SCHOOL",
83
+ isHidden: false,
84
+ schoolId: { in: input.schoolIds },
85
+ },
86
+ // Authors can always see their own listings (including hidden).
87
+ ...(input.authorId ? [{ authorId: input.authorId }] : []),
88
+ ],
89
+ },
90
+ ...(input.type ? [{ type: input.type }] : []),
91
+ ...(input.tags?.length
92
+ ? [{ tags: { hasSome: input.tags } }]
93
+ : []),
94
+ ...(search
95
+ ? [
96
+ {
97
+ OR: [
98
+ { title: { contains: search, mode: "insensitive" as const } },
99
+ {
100
+ description: {
101
+ contains: search,
102
+ mode: "insensitive" as const,
103
+ },
104
+ },
105
+ { tags: { has: search } },
106
+ ],
107
+ },
108
+ ]
109
+ : []),
110
+ ],
111
+ };
112
+
113
+ const rows = await prisma.marketplaceListing.findMany({
114
+ where,
115
+ include: marketplaceListingInclude,
116
+ orderBy: [{ createdAt: "desc" }, { id: "desc" }],
117
+ take: input.limit + 1,
118
+ ...(input.cursor
119
+ ? {
120
+ cursor: { id: input.cursor },
121
+ skip: 1,
122
+ }
123
+ : {}),
124
+ });
125
+
126
+ let nextCursor: string | undefined;
127
+ if (rows.length > input.limit) {
128
+ const next = rows.pop();
129
+ nextCursor = next?.id;
130
+ }
131
+
132
+ return { items: rows, nextCursor };
133
+ }
134
+
135
+ export async function incrementMarketplaceReportCount(id: string) {
136
+ return prisma.marketplaceListing.update({
137
+ where: { id },
138
+ data: { reportCount: { increment: 1 } },
139
+ select: { id: true, reportCount: true, isHidden: true },
140
+ });
141
+ }
142
+
143
+ export async function setMarketplaceListingHidden(
144
+ id: string,
145
+ isHidden: boolean
146
+ ) {
147
+ return prisma.marketplaceListing.update({
148
+ where: { id },
149
+ data: { isHidden },
150
+ include: marketplaceListingInclude,
151
+ });
152
+ }
153
+
154
+ export async function deleteMarketplaceListing(id: string, authorId: string) {
155
+ return prisma.marketplaceListing.deleteMany({
156
+ where: { id, authorId },
157
+ });
158
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * ModuleItem model – Canvas-style pages / file collections / videos in a section.
3
+ */
4
+ import { prisma } from "../lib/prisma.js";
5
+ import type { ModuleItemType } from "@prisma/client";
6
+ import {
7
+ classMemberOrSchoolAdminWhere,
8
+ classTeacherOrSchoolAdminWhere,
9
+ } from "./access.js";
10
+
11
+ const moduleItemInclude = {
12
+ files: {
13
+ select: {
14
+ id: true,
15
+ name: true,
16
+ type: true,
17
+ size: true,
18
+ path: true,
19
+ uploadStatus: true,
20
+ },
21
+ },
22
+ section: {
23
+ select: { id: true, name: true, classId: true, color: true },
24
+ },
25
+ } as const;
26
+
27
+ export function findModuleItemsByClassId(classId: string) {
28
+ return prisma.moduleItem.findMany({
29
+ where: { section: { classId } },
30
+ include: moduleItemInclude,
31
+ orderBy: [{ section: { order: "asc" } }, { order: "asc" }],
32
+ });
33
+ }
34
+
35
+ export function findModuleItemsBySectionId(sectionId: string) {
36
+ return prisma.moduleItem.findMany({
37
+ where: { sectionId },
38
+ include: moduleItemInclude,
39
+ orderBy: { order: "asc" },
40
+ });
41
+ }
42
+
43
+ export function findModuleItemByIdForMember(id: string, userId: string) {
44
+ return prisma.moduleItem.findFirst({
45
+ where: {
46
+ id,
47
+ section: { class: classMemberOrSchoolAdminWhere(userId) },
48
+ },
49
+ include: moduleItemInclude,
50
+ });
51
+ }
52
+
53
+ export function findModuleItemByIdForTeacher(id: string, userId: string) {
54
+ return prisma.moduleItem.findFirst({
55
+ where: {
56
+ id,
57
+ section: { class: classTeacherOrSchoolAdminWhere(userId) },
58
+ },
59
+ include: moduleItemInclude,
60
+ });
61
+ }
62
+
63
+ export function createModuleItem(data: {
64
+ sectionId: string;
65
+ title: string;
66
+ type: ModuleItemType;
67
+ body?: string | null;
68
+ order?: number | null;
69
+ published?: boolean;
70
+ }) {
71
+ return prisma.moduleItem.create({
72
+ data,
73
+ include: moduleItemInclude,
74
+ });
75
+ }
76
+
77
+ export function updateModuleItem(
78
+ id: string,
79
+ data: {
80
+ title?: string;
81
+ body?: string | null;
82
+ order?: number | null;
83
+ published?: boolean;
84
+ type?: ModuleItemType;
85
+ sectionId?: string;
86
+ }
87
+ ) {
88
+ return prisma.moduleItem.update({
89
+ where: { id },
90
+ data,
91
+ include: moduleItemInclude,
92
+ });
93
+ }
94
+
95
+ export function deleteModuleItem(id: string) {
96
+ return prisma.moduleItem.delete({ where: { id } });
97
+ }
98
+
99
+ export function attachFilesToModuleItem(
100
+ moduleItemId: string,
101
+ fileIds: string[]
102
+ ) {
103
+ return prisma.file.updateMany({
104
+ where: { id: { in: fileIds } },
105
+ data: { moduleItemId },
106
+ });
107
+ }
@@ -56,6 +56,8 @@ export function listUserSchoolMemberships(userId: string) {
56
56
  subdomain: true,
57
57
  logoId: true,
58
58
  logo: { select: { id: true, path: true, type: true } },
59
+ letterheadFileId: true,
60
+ letterhead: { select: { id: true, path: true, type: true, name: true } },
59
61
  createdAt: true,
60
62
  updatedAt: true,
61
63
  },
@@ -102,6 +104,8 @@ export function getSchoolById(schoolId: string) {
102
104
  subdomain: true,
103
105
  logoId: true,
104
106
  logo: { select: { id: true, name: true, type: true, path: true } },
107
+ letterheadFileId: true,
108
+ letterhead: { select: { id: true, name: true, type: true, path: true } },
105
109
  createdAt: true,
106
110
  updatedAt: true,
107
111
  },
@@ -115,6 +119,7 @@ export function updateSchoolRecord(
115
119
  description?: string | null;
116
120
  subdomain?: string | null;
117
121
  logoId?: string | null;
122
+ letterheadFileId?: string | null;
118
123
  }
119
124
  ) {
120
125
  return prisma.school.update({
@@ -126,6 +131,7 @@ export function updateSchoolRecord(
126
131
  description: true,
127
132
  subdomain: true,
128
133
  logoId: true,
134
+ letterheadFileId: true,
129
135
  },
130
136
  });
131
137
  }
@@ -190,6 +190,7 @@ export function createWorksheetQuestion(data: {
190
190
  options?: object;
191
191
  markScheme?: object;
192
192
  order?: number;
193
+ parentQuestionId?: string | null;
193
194
  }) {
194
195
  return prisma.worksheetQuestion.create({
195
196
  data,
@@ -214,6 +215,7 @@ export function updateWorksheetQuestion(
214
215
  options?: object;
215
216
  markScheme?: object;
216
217
  type?: WorksheetQuestionType;
218
+ parentQuestionId?: string | null;
217
219
  }
218
220
  ) {
219
221
  return prisma.worksheetQuestion.update({
@@ -30,6 +30,11 @@ import { discussionRouter } from "./discussion.js";
30
30
  import { adminRouter } from "./admin.js";
31
31
  import { embeddingRouter } from "./embedding.js";
32
32
  import { gradebookRouter } from "./gradebook.js";
33
+ import { feesRouter } from "./fees.js";
34
+ import { marketplaceRouter } from "./marketplace.js";
35
+ import { moduleItemRouter } from "./moduleItem.js";
36
+ import { academicCalendarRouter } from "./academicCalendar.js";
37
+ import { parentRouter } from "./parent.js";
33
38
 
34
39
  export const appRouter = createTRPCRouter({
35
40
  class: classRouter,
@@ -61,6 +66,11 @@ export const appRouter = createTRPCRouter({
61
66
  admin: adminRouter,
62
67
  embedding: embeddingRouter,
63
68
  gradebook: gradebookRouter,
69
+ fees: feesRouter,
70
+ marketplace: marketplaceRouter,
71
+ moduleItem: moduleItemRouter,
72
+ academicCalendar: academicCalendarRouter,
73
+ parent: parentRouter,
64
74
  });
65
75
 
66
76
  // Export type router type definition