@playcademy/vite-plugin 0.2.29 → 0.2.30-beta.2

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 (2) hide show
  1. package/dist/index.js +243 -183
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25370,7 +25370,7 @@ var package_default;
25370
25370
  var init_package = __esm(() => {
25371
25371
  package_default = {
25372
25372
  name: "@playcademy/sandbox",
25373
- version: "0.3.16",
25373
+ version: "0.3.17-beta.35",
25374
25374
  description: "Local development server for Playcademy game development",
25375
25375
  type: "module",
25376
25376
  exports: {
@@ -54340,6 +54340,31 @@ function resolveAdminEventTime(data) {
54340
54340
  }
54341
54341
  return toAttributionEventTime(data.date);
54342
54342
  }
54343
+ function validateMasteryAdjustment(delta, currentMastered, masterableUnits) {
54344
+ if (delta < 0 && currentMastered + delta < 0) {
54345
+ throw new ValidationError(`Adjustment would go below 0. Current: ${currentMastered}, adjustment: ${delta}`);
54346
+ }
54347
+ if (delta < 0 && typeof masterableUnits === "number" && masterableUnits > 0 && currentMastered > masterableUnits && currentMastered + delta > masterableUnits) {
54348
+ const minDelta = masterableUnits - currentMastered;
54349
+ throw new ValidationError(`Adjustment must reduce mastery to at most ${masterableUnits}. Current: ${currentMastered}/${masterableUnits}, minimum adjustment: ${minDelta}`);
54350
+ }
54351
+ if (delta > 0 && typeof masterableUnits === "number" && masterableUnits > 0) {
54352
+ if (currentMastered >= masterableUnits) {
54353
+ throw new ValidationError(`Mastery is already at maximum (${currentMastered}/${masterableUnits}). Only negative adjustments are allowed.`);
54354
+ }
54355
+ if (currentMastered + delta > masterableUnits) {
54356
+ const remaining = masterableUnits - currentMastered;
54357
+ throw new ValidationError(`Adjustment would exceed maximum. Current: ${currentMastered}/${masterableUnits}, max adjustment: +${remaining}`);
54358
+ }
54359
+ }
54360
+ }
54361
+ function compareEnrollmentsByRecency(a, b) {
54362
+ const dateCompare = (b.beginDate ?? "").localeCompare(a.beginDate ?? "");
54363
+ if (dateCompare !== 0) {
54364
+ return dateCompare;
54365
+ }
54366
+ return (b.dateLastModified ?? "").localeCompare(a.dateLastModified ?? "");
54367
+ }
54343
54368
  var init_timeback_admin_util = __esm(() => {
54344
54369
  init_errors();
54345
54370
  });
@@ -54689,17 +54714,6 @@ class TimebackAdminService {
54689
54714
  }
54690
54715
  return this.deps.timeback;
54691
54716
  }
54692
- async recordCourseCompletionHistory(client, data) {
54693
- await client.recordAdminCourseCompletionChange(data).catch((error) => {
54694
- logger16.error("Failed to record admin course completion history event", {
54695
- gameId: data.gameId,
54696
- courseId: data.courseId,
54697
- studentId: data.studentId,
54698
- action: data.action,
54699
- error: error instanceof Error ? error.message : String(error)
54700
- });
54701
- });
54702
- }
54703
54717
  async resolveAdminMutationContext(gameId, courseId, user, studentId, accessLevel = "developer") {
54704
54718
  const client = this.requireClient();
54705
54719
  if (accessLevel === "dashboard") {
@@ -54798,7 +54812,7 @@ class TimebackAdminService {
54798
54812
  if (typeof masterableUnits !== "number" || masterableUnits <= 0) {
54799
54813
  return;
54800
54814
  }
54801
- return Math.min(100, Math.round(masteredUnits / masterableUnits * 100));
54815
+ return Math.round(masteredUnits / masterableUnits * 100);
54802
54816
  }
54803
54817
  async getMasterableUnits(courseId) {
54804
54818
  const client = this.requireClient();
@@ -54851,6 +54865,7 @@ class TimebackAdminService {
54851
54865
  }
54852
54866
  async getStudentEnrollmentsByCourseId(client, studentId, courseIds, options) {
54853
54867
  const enrollments = new Map;
54868
+ const allEnrollments = new Map;
54854
54869
  const entries = await Promise.all(courseIds.map(async (courseId) => {
54855
54870
  const roster = await client.oneroster.enrollments.listByCourse(courseId, {
54856
54871
  includeInactive: options?.includeInactive,
@@ -54862,28 +54877,30 @@ class TimebackAdminService {
54862
54877
  if (aActive !== bActive) {
54863
54878
  return aActive ? -1 : 1;
54864
54879
  }
54865
- return (b.enrollment.dateLastModified ?? "").localeCompare(a.enrollment.dateLastModified ?? "");
54880
+ return compareEnrollmentsByRecency(a.enrollment, b.enrollment);
54866
54881
  });
54867
- return { courseId, match: matches[0] ?? null };
54882
+ return { courseId, matches };
54868
54883
  }));
54869
- for (const { courseId, match } of entries) {
54870
- if (match) {
54871
- enrollments.set(courseId, {
54872
- id: match.enrollment.sourcedId,
54873
- status: match.enrollment.status ?? "active",
54874
- role: match.enrollment.role ?? "student",
54875
- beginDate: match.enrollment.beginDate ?? null,
54876
- endDate: match.enrollment.endDate ?? null,
54877
- course: {
54878
- id: courseId,
54879
- title: match.class?.title ?? "",
54880
- subjects: null,
54881
- grades: null
54882
- }
54883
- });
54884
+ for (const { courseId, matches } of entries) {
54885
+ const records = matches.map((match) => ({
54886
+ id: match.enrollment.sourcedId,
54887
+ status: match.enrollment.status ?? "active",
54888
+ role: match.enrollment.role ?? "student",
54889
+ beginDate: match.enrollment.beginDate ?? null,
54890
+ endDate: match.enrollment.endDate ?? null,
54891
+ course: {
54892
+ id: courseId,
54893
+ title: match.class?.title ?? "",
54894
+ subjects: null,
54895
+ grades: null
54896
+ }
54897
+ }));
54898
+ if (records.length > 0) {
54899
+ enrollments.set(courseId, records[0]);
54884
54900
  }
54901
+ allEnrollments.set(courseId, records);
54885
54902
  }
54886
- return { enrollments };
54903
+ return { enrollments, allEnrollments };
54887
54904
  }
54888
54905
  async assertStudentEnrolledInCourse(client, studentId, courseId) {
54889
54906
  const enrollments = await client.edubridge.enrollments.listByUser(studentId);
@@ -54970,7 +54987,7 @@ class TimebackAdminService {
54970
54987
  const enrollmentId = rosterEntry.enrollment.sourcedId || null;
54971
54988
  const summary = enrollmentId ? analyticsByEnrollmentId.get(enrollmentId) : undefined;
54972
54989
  const analyticsUnavailable = Boolean(enrollmentId) && summary?.analyticsAvailable !== true;
54973
- const name3 = rosterEntry.user ? `${rosterEntry.user.givenName} ${rosterEntry.user.familyName}`.trim() : rosterEntry.enrollment.user.sourcedId;
54990
+ const name3 = rosterEntry.user ? `${rosterEntry.user.givenName} ${rosterEntry.user.familyName}`.trim() || "No name specified" : "No name specified";
54974
54991
  const inactive = rosterEntry.enrollment.status === "tobedeleted";
54975
54992
  return {
54976
54993
  studentId: rosterEntry.enrollment.user.sourcedId,
@@ -55015,14 +55032,15 @@ class TimebackAdminService {
55015
55032
  throw new NotFoundError("Timeback integration", gameId);
55016
55033
  }
55017
55034
  const courseIds = new Set(integrations.map((integration) => integration.courseId));
55018
- const { enrollments: enrollmentsByCourseId } = await this.getStudentEnrollmentsByCourseId(client, studentId, [...courseIds], {
55035
+ const { enrollments: enrollmentsByCourseId, allEnrollments: allEnrollmentsByCourseId } = await this.getStudentEnrollmentsByCourseId(client, studentId, [...courseIds], {
55019
55036
  includeInactive: true
55020
55037
  });
55021
55038
  if (enrollmentsByCourseId.size === 0) {
55022
55039
  throw new NotFoundError("Student enrollment", courseId ? `${studentId}:${courseId}` : `${studentId}:${gameId}`);
55023
55040
  }
55041
+ const allEnrollmentIds = [...allEnrollmentsByCourseId.values()].flat().map((enrollment) => enrollment.id);
55024
55042
  const studentProfile = await client.oneroster.users.get(studentId);
55025
- const analyticsByEnrollmentId = await this.loadEnrollmentAnalyticsSummaries([...enrollmentsByCourseId.values()].map((enrollment) => enrollment.id));
55043
+ const analyticsByEnrollmentId = await this.loadEnrollmentAnalyticsSummaries(allEnrollmentIds);
55026
55044
  const [masterableUnitsByCourse, completionStatusByCourse] = await Promise.all([
55027
55045
  this.getMasterableUnitsByCourse(integrations.map((integration) => integration.courseId)),
55028
55046
  this.getCompletionStatusByCourse(client, integrations.map((integration) => integration.courseId), studentId)
@@ -55033,6 +55051,24 @@ class TimebackAdminService {
55033
55051
  const masterableUnits = masterableUnitsByCourse.get(integration.courseId);
55034
55052
  const analyticsUnavailable = Boolean(enrollment?.id) && summary?.analyticsAvailable !== true;
55035
55053
  const inactive = enrollment?.status === "tobedeleted";
55054
+ const courseEnrollments = allEnrollmentsByCourseId.get(integration.courseId) ?? [];
55055
+ const enrollmentSummaries = courseEnrollments.length > 1 ? courseEnrollments.map((record) => {
55056
+ const recordSummary = analyticsByEnrollmentId.get(record.id);
55057
+ const recordAnalyticsUnavailable = recordSummary?.analyticsAvailable !== true;
55058
+ return {
55059
+ enrollmentId: record.id,
55060
+ status: record.status === "tobedeleted" ? "tobedeleted" : "active",
55061
+ beginDate: record.beginDate,
55062
+ endDate: record.endDate,
55063
+ analyticsUnavailable: recordAnalyticsUnavailable,
55064
+ totalXp: recordSummary?.totalXp ?? 0,
55065
+ todayXp: recordSummary?.todayXp ?? 0,
55066
+ activeTimeSeconds: recordSummary?.activeTimeSeconds ?? 0,
55067
+ masteredUnits: recordSummary?.masteredUnits ?? 0,
55068
+ pctCompleteApp: TimebackAdminService.computeCompletionPct(recordSummary?.masteredUnits ?? 0, masterableUnits),
55069
+ history: recordSummary?.history ?? []
55070
+ };
55071
+ }) : undefined;
55036
55072
  return {
55037
55073
  courseId: integration.courseId,
55038
55074
  title: enrollment?.course.title || `${integration.subject} Grade ${integration.grade}`,
@@ -55048,9 +55084,11 @@ class TimebackAdminService {
55048
55084
  pctCompleteApp: TimebackAdminService.computeCompletionPct(summary?.masteredUnits ?? 0, masterableUnits),
55049
55085
  completionStatus: completionStatusByCourse.get(integration.courseId) ?? "none",
55050
55086
  history: summary?.history ?? [],
55051
- ...inactive ? { inactive } : {}
55087
+ ...inactive ? { inactive } : {},
55088
+ ...enrollmentSummaries ? { enrollments: enrollmentSummaries } : {}
55052
55089
  };
55053
55090
  });
55091
+ courses.sort((a, b) => a.grade - b.grade);
55054
55092
  return {
55055
55093
  student: {
55056
55094
  studentId,
@@ -55150,111 +55188,87 @@ class TimebackAdminService {
55150
55188
  return { status: "ok" };
55151
55189
  }
55152
55190
  async adjustMasteredUnits(data, user) {
55153
- const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
55191
+ const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user);
55192
+ let currentMastered = 0;
55193
+ const masterableUnits = await this.getMasterableUnits(data.courseId);
55194
+ if (data.units !== 0) {
55195
+ const enrollment = await this.assertStudentEnrolledInCourse(client, data.studentId, data.courseId);
55196
+ try {
55197
+ const analytics = await client.edubridge.analytics.getEnrollmentFacts(enrollment.id, { timezone: PLATFORM_TIMEZONE });
55198
+ currentMastered = this.summarizeAnalyticsFacts(analytics.facts).masteredUnits;
55199
+ } catch {
55200
+ throw new ValidationError("Unable to validate mastery bounds — analytics unavailable. Please retry.");
55201
+ }
55202
+ validateMasteryAdjustment(data.units, currentMastered, masterableUnits);
55203
+ }
55204
+ const pctCompleteApp = typeof masterableUnits === "number" && masterableUnits > 0 ? Math.min(100, Math.max(0, Math.round((currentMastered + data.units) / masterableUnits * 100))) : undefined;
55154
55205
  await client.recordAdminMasteryAdjustment({
55155
55206
  gameId: data.gameId,
55156
55207
  courseId: data.courseId,
55157
55208
  studentId: data.studentId,
55158
55209
  masteredUnits: data.units,
55210
+ pctCompleteApp,
55159
55211
  eventTime: resolveAdminEventTime(data),
55160
55212
  reason: data.reason,
55161
55213
  actor,
55162
55214
  appName,
55163
55215
  sensorUrl
55164
55216
  });
55165
- return { status: "ok" };
55166
- }
55167
- async toggleCourseCompletion(data, user) {
55168
- const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId, "dashboard");
55169
- const historyClient = client;
55170
- const ids = deriveSourcedIds(data.courseId);
55171
- const lineItemId = `${ids.course}-mastery-completion-assessment`;
55172
- const resultId = `${lineItemId}:${data.studentId}:completion`;
55173
- await client.oneroster.assessmentLineItems.findOrCreate(lineItemId, {
55174
- sourcedId: lineItemId,
55175
- title: "Mastery Completion",
55176
- status: ONEROSTER_STATUS.active,
55177
- course: { sourcedId: ids.course },
55178
- ...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
55179
- });
55180
- if (data.action === "complete") {
55181
- const masterableUnits = await this.getMasterableUnits(data.courseId);
55182
- if (masterableUnits && masterableUnits > 0) {
55183
- const enrollment = await this.assertStudentEnrolledInCourse(client, data.studentId, data.courseId);
55184
- let currentMastered = 0;
55185
- try {
55186
- const analytics = await client.edubridge.analytics.getEnrollmentFacts(enrollment.id, { timezone: PLATFORM_TIMEZONE });
55187
- const summary = this.summarizeAnalyticsFacts(analytics.facts);
55188
- currentMastered = summary.masteredUnits;
55189
- } catch {
55190
- logger16.warn("Failed to load analytics for mastery gap calculation", {
55191
- studentId: data.studentId,
55192
- courseId: data.courseId
55217
+ if (typeof masterableUnits === "number" && masterableUnits > 0) {
55218
+ const wasMastered = currentMastered >= masterableUnits;
55219
+ const willBeMastered = currentMastered + data.units >= masterableUnits;
55220
+ if (wasMastered !== willBeMastered) {
55221
+ const ids = deriveSourcedIds(data.courseId);
55222
+ const lineItemId = `${ids.course}-mastery-completion-assessment`;
55223
+ const resultId = `${lineItemId}:${data.studentId}:completion`;
55224
+ if (willBeMastered) {
55225
+ await client.oneroster.assessmentLineItems.findOrCreate(lineItemId, {
55226
+ sourcedId: lineItemId,
55227
+ title: "Mastery Completion",
55228
+ status: ONEROSTER_STATUS.active,
55229
+ course: { sourcedId: ids.course },
55230
+ ...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
55193
55231
  });
55194
- }
55195
- const gap = masterableUnits - currentMastered;
55196
- if (gap > 0) {
55197
- await client.recordAdminMasteryAdjustment({
55198
- gameId: data.gameId,
55199
- courseId: data.courseId,
55200
- studentId: data.studentId,
55201
- masteredUnits: gap,
55202
- reason: "Admin completed course",
55203
- actor,
55204
- appName,
55205
- sensorUrl
55232
+ await client.oneroster.assessmentResults.upsert(resultId, {
55233
+ sourcedId: resultId,
55234
+ status: ONEROSTER_STATUS.active,
55235
+ assessmentLineItem: { sourcedId: lineItemId },
55236
+ student: { sourcedId: data.studentId },
55237
+ score: 100,
55238
+ scoreDate: new Date().toISOString(),
55239
+ scoreStatus: SCORE_STATUS.fullyGraded,
55240
+ inProgress: "false",
55241
+ metadata: {
55242
+ isMasteryCompletion: true,
55243
+ adminAction: true,
55244
+ appName
55245
+ }
55206
55246
  });
55247
+ } else {
55248
+ try {
55249
+ await client.oneroster.assessmentResults.upsert(resultId, {
55250
+ sourcedId: resultId,
55251
+ status: ONEROSTER_STATUS.active,
55252
+ assessmentLineItem: { sourcedId: lineItemId },
55253
+ student: { sourcedId: data.studentId },
55254
+ score: 0,
55255
+ scoreDate: new Date().toISOString(),
55256
+ scoreStatus: SCORE_STATUS.notSubmitted,
55257
+ inProgress: "true",
55258
+ metadata: {
55259
+ isMasteryCompletion: true,
55260
+ adminAction: true,
55261
+ appName
55262
+ }
55263
+ });
55264
+ } catch {
55265
+ logger16.debug("No completion entry to revoke", {
55266
+ studentId: data.studentId,
55267
+ courseId: data.courseId
55268
+ });
55269
+ }
55207
55270
  }
55208
55271
  }
55209
- await client.oneroster.assessmentResults.upsert(resultId, {
55210
- sourcedId: resultId,
55211
- status: ONEROSTER_STATUS.active,
55212
- assessmentLineItem: { sourcedId: lineItemId },
55213
- student: { sourcedId: data.studentId },
55214
- score: 100,
55215
- scoreDate: new Date().toISOString(),
55216
- scoreStatus: SCORE_STATUS.fullyGraded,
55217
- inProgress: "false",
55218
- metadata: {
55219
- isMasteryCompletion: true,
55220
- adminAction: true,
55221
- appName
55222
- }
55223
- });
55224
- await this.recordCourseCompletionHistory(historyClient, {
55225
- gameId: data.gameId,
55226
- courseId: data.courseId,
55227
- studentId: data.studentId,
55228
- action: "complete",
55229
- actor,
55230
- appName,
55231
- sensorUrl
55232
- });
55233
- } else {
55234
- await client.oneroster.assessmentResults.upsert(resultId, {
55235
- sourcedId: resultId,
55236
- status: ONEROSTER_STATUS.active,
55237
- assessmentLineItem: { sourcedId: lineItemId },
55238
- student: { sourcedId: data.studentId },
55239
- score: 0,
55240
- scoreDate: new Date().toISOString(),
55241
- scoreStatus: SCORE_STATUS.notSubmitted,
55242
- inProgress: "true",
55243
- metadata: {
55244
- isMasteryCompletion: true,
55245
- adminAction: true,
55246
- appName
55247
- }
55248
- });
55249
- await this.recordCourseCompletionHistory(historyClient, {
55250
- gameId: data.gameId,
55251
- courseId: data.courseId,
55252
- studentId: data.studentId,
55253
- action: "resume",
55254
- actor,
55255
- appName,
55256
- sensorUrl
55257
- });
55258
55272
  }
55259
55273
  return { status: "ok" };
55260
55274
  }
@@ -55290,17 +55304,34 @@ class TimebackAdminService {
55290
55304
  });
55291
55305
  return { students: [] };
55292
55306
  }
55293
- const roster = await client.oneroster.enrollments.listByCourse(courseId, {
55307
+ const fullRoster = await client.oneroster.enrollments.listByCourse(courseId, {
55294
55308
  role: "student",
55309
+ includeInactive: true,
55295
55310
  includeUsers: false
55296
55311
  });
55297
- const enrolledStudentIds = new Set(roster.map((entry) => entry.enrollment.user.sourcedId));
55298
- const students = allUsers.filter((entry) => Boolean(entry.sourcedId) && entry.roles?.some((role) => role.role === "student") === true).map((entry) => ({
55299
- studentId: entry.sourcedId,
55300
- name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || entry.sourcedId,
55301
- email: entry.email || null,
55302
- alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId)
55303
- }));
55312
+ const enrolledStudentIds = new Set(fullRoster.filter((entry) => entry.enrollment.status === "active").map((entry) => entry.enrollment.user.sourcedId));
55313
+ const pastEnrollmentsByStudent = new Map;
55314
+ const inactiveEntries = fullRoster.filter((entry) => entry.enrollment.status === "tobedeleted").toSorted((a, b) => compareEnrollmentsByRecency(a.enrollment, b.enrollment));
55315
+ for (const entry of inactiveEntries) {
55316
+ const studentId = entry.enrollment.user.sourcedId;
55317
+ const list = pastEnrollmentsByStudent.get(studentId) ?? [];
55318
+ list.push({
55319
+ enrollmentId: entry.enrollment.sourcedId,
55320
+ beginDate: entry.enrollment.beginDate ?? null,
55321
+ endDate: entry.enrollment.endDate ?? null
55322
+ });
55323
+ pastEnrollmentsByStudent.set(studentId, list);
55324
+ }
55325
+ const students = allUsers.filter((entry) => Boolean(entry.sourcedId) && entry.roles?.some((role) => role.role === "student") === true).map((entry) => {
55326
+ const past = pastEnrollmentsByStudent.get(entry.sourcedId) ?? [];
55327
+ return {
55328
+ studentId: entry.sourcedId,
55329
+ name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || "No name specified",
55330
+ email: entry.email || null,
55331
+ alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId),
55332
+ ...past.length > 0 ? { pastEnrollments: past } : {}
55333
+ };
55334
+ });
55304
55335
  return { students };
55305
55336
  }
55306
55337
  async enrollStudent(data, user) {
@@ -55331,6 +55362,48 @@ class TimebackAdminService {
55331
55362
  client.invalidateEnrollments(data.studentId);
55332
55363
  return { status: "ok" };
55333
55364
  }
55365
+ async reactivateEnrollment(data, user) {
55366
+ const client = this.requireClient();
55367
+ await this.deps.validateGameManagementAccess(user, data.gameId);
55368
+ const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
55369
+ where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
55370
+ });
55371
+ if (!integration) {
55372
+ throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
55373
+ }
55374
+ const enrollment = await client.oneroster.enrollments.get(data.enrollmentId);
55375
+ if (!enrollment) {
55376
+ throw new NotFoundError("Enrollment", data.enrollmentId);
55377
+ }
55378
+ if (enrollment.user.sourcedId !== data.studentId) {
55379
+ throw new ValidationError("Enrollment does not belong to the specified student");
55380
+ }
55381
+ if (enrollment.status === "active") {
55382
+ throw new ValidationError("Enrollment is already active");
55383
+ }
55384
+ const { allEnrollments } = await this.getStudentEnrollmentsByCourseId(client, data.studentId, [data.courseId], { includeInactive: true });
55385
+ const courseEnrollmentIds = new Set((allEnrollments.get(data.courseId) ?? []).map((e) => e.id));
55386
+ if (!courseEnrollmentIds.has(data.enrollmentId)) {
55387
+ throw new ValidationError("Enrollment does not belong to the specified course");
55388
+ }
55389
+ const activeEnrollments = await client.edubridge.enrollments.listByUser(data.studentId);
55390
+ if (activeEnrollments.some((e) => e.course.id === data.courseId)) {
55391
+ throw new ValidationError("Student already has an active enrollment for this course. Unenroll from the current enrollment before reactivating a past one.");
55392
+ }
55393
+ await client.oneroster.enrollments.update(data.enrollmentId, {
55394
+ role: enrollment.role,
55395
+ primary: enrollment.primary,
55396
+ beginDate: enrollment.beginDate,
55397
+ endDate: enrollment.endDate,
55398
+ user: enrollment.user,
55399
+ class: enrollment.class,
55400
+ school: enrollment.school,
55401
+ sourcedId: data.enrollmentId,
55402
+ status: "active"
55403
+ });
55404
+ client.invalidateEnrollments(data.studentId);
55405
+ return { status: "ok" };
55406
+ }
55334
55407
  async getCompletionStatus(client, courseId, studentId) {
55335
55408
  const ids = deriveSourcedIds(courseId);
55336
55409
  const lineItemId = `${ids.course}-mastery-completion-assessment`;
@@ -60403,6 +60476,10 @@ function createOneRosterNamespace(client) {
60403
60476
  }
60404
60477
  },
60405
60478
  enrollments: {
60479
+ get: async (sourcedId) => {
60480
+ const response = await client["request"](`${ONEROSTER_ENDPOINTS5.enrollments}/${sourcedId}`, "GET");
60481
+ return response.enrollment;
60482
+ },
60406
60483
  listByClass: async (classSourcedId, options) => {
60407
60484
  const queryParams = new URLSearchParams;
60408
60485
  const filters = [`class.sourcedId='${escapeFilterValue2(classSourcedId)}'`];
@@ -60480,6 +60557,11 @@ function createOneRosterNamespace(client) {
60480
60557
  }
60481
60558
  },
60482
60559
  create: async (data) => client["request"](ONEROSTER_ENDPOINTS5.enrollments, "POST", { enrollment: data }),
60560
+ update: async (sourcedId, data) => {
60561
+ await client["request"](`${ONEROSTER_ENDPOINTS5.enrollments}/${sourcedId}`, "PUT", {
60562
+ enrollment: data
60563
+ });
60564
+ },
60483
60565
  delete: async (sourcedId) => {
60484
60566
  await client["request"](`${ONEROSTER_ENDPOINTS5.enrollments}/${sourcedId}`, "DELETE");
60485
60567
  }
@@ -60829,6 +60911,7 @@ class AdminEventRecorder {
60829
60911
  defaultActivityId: "playcademy-admin-mastery-adjustment",
60830
60912
  eventKind: "remediation-mastery"
60831
60913
  });
60914
+ const courseUrl = createOneRosterUrls(TIMEBACK_API_URLS5[this.environment]).course(data.courseId);
60832
60915
  await this.caliper.emitActivityEvent({
60833
60916
  studentId: ctx.student.id,
60834
60917
  studentEmail: ctx.student.email,
@@ -60843,32 +60926,13 @@ class AdminEventRecorder {
60843
60926
  appName: ctx.appName,
60844
60927
  sensorUrl: ctx.sensorUrl,
60845
60928
  process: true,
60846
- generatedExtensions: ctx.metadata,
60847
- eventExtensions: ctx.metadata
60848
- });
60849
- }
60850
- async recordCourseCompletionChange(data) {
60851
- const isResume = data.action === "resume";
60852
- const ctx = await this.prepareAdminEvent({
60853
- ...data,
60854
- defaultActivityId: isResume ? "playcademy-admin-course-resumed" : "playcademy-admin-course-completed",
60855
- reason: "Admin action",
60856
- eventKind: isResume ? "course-resumed" : "course-completed"
60857
- });
60858
- await this.caliper.emitActivityEvent({
60859
- studentId: ctx.student.id,
60860
- studentEmail: ctx.student.email,
60861
- gameId: data.gameId,
60862
- activityId: ctx.activityId,
60863
- activityName: isResume ? "Course resumed" : "Course marked complete",
60864
- courseId: data.courseId,
60865
- courseName: ctx.courseContext.courseName,
60866
- subject: ctx.courseContext.subject,
60867
- appName: ctx.appName,
60868
- sensorUrl: ctx.sensorUrl,
60869
- process: false,
60870
60929
  includeAttempt: false,
60871
- generatedExtensions: ctx.metadata,
60930
+ objectId: `${ctx.sensorUrl.replace(/\/$/, "")}/urn:uuid:${crypto.randomUUID()}`,
60931
+ generatedId: courseUrl,
60932
+ generatedExtensions: {
60933
+ ...ctx.metadata,
60934
+ ...data.pctCompleteApp !== undefined ? { pctCompleteApp: data.pctCompleteApp } : {}
60935
+ },
60872
60936
  eventExtensions: ctx.metadata
60873
60937
  });
60874
60938
  }
@@ -62059,10 +62123,6 @@ class TimebackClient {
62059
62123
  await this._ensureAuthenticated();
62060
62124
  return this.adminEventRecorder.recordMasteryAdjustment(data);
62061
62125
  }
62062
- async recordAdminCourseCompletionChange(data) {
62063
- await this._ensureAuthenticated();
62064
- return this.adminEventRecorder.recordCourseCompletionChange(data);
62065
- }
62066
62126
  clearCaches() {
62067
62127
  this.cacheManager.clearAll();
62068
62128
  }
@@ -121696,9 +121756,9 @@ var AdminAttributionDateSchema;
121696
121756
  var GrantTimebackXpRequestSchema;
121697
121757
  var AdjustTimebackTimeRequestSchema;
121698
121758
  var AdjustTimebackMasteryRequestSchema;
121699
- var ToggleCourseCompletionRequestSchema;
121700
121759
  var EnrollStudentRequestSchema;
121701
121760
  var UnenrollStudentRequestSchema;
121761
+ var ReactivateEnrollmentRequestSchema;
121702
121762
  var InsertAssessmentTestSchema;
121703
121763
  var CreateAssessmentRequestSchema;
121704
121764
  var ReorderAssessmentsRequestSchema;
@@ -121873,12 +121933,6 @@ var init_schemas11 = __esm(() => {
121873
121933
  date: AdminAttributionDateSchema.optional(),
121874
121934
  useCurrentTime: exports_external.boolean().optional()
121875
121935
  });
121876
- ToggleCourseCompletionRequestSchema = exports_external.object({
121877
- gameId: exports_external.string().uuid(),
121878
- courseId: exports_external.string().min(1),
121879
- studentId: exports_external.string().min(1),
121880
- action: exports_external.enum(["complete", "resume"])
121881
- });
121882
121936
  EnrollStudentRequestSchema = exports_external.object({
121883
121937
  gameId: exports_external.string().uuid(),
121884
121938
  courseId: exports_external.string().min(1),
@@ -121889,6 +121943,12 @@ var init_schemas11 = __esm(() => {
121889
121943
  courseId: exports_external.string().min(1),
121890
121944
  studentId: exports_external.string().min(1)
121891
121945
  });
121946
+ ReactivateEnrollmentRequestSchema = exports_external.object({
121947
+ gameId: exports_external.string().uuid(),
121948
+ courseId: exports_external.string().min(1),
121949
+ studentId: exports_external.string().min(1),
121950
+ enrollmentId: exports_external.string().min(1)
121951
+ });
121892
121952
  InsertAssessmentTestSchema = createInsertSchema(gameTimebackAssessmentTests).omit({
121893
121953
  id: true,
121894
121954
  createdAt: true
@@ -124138,10 +124198,10 @@ var getActivityDetail;
124138
124198
  var grantXp;
124139
124199
  var adjustTime;
124140
124200
  var adjustMastery;
124141
- var toggleCompletion;
124142
124201
  var searchStudents;
124143
124202
  var enrollStudent;
124144
124203
  var unenrollStudent;
124204
+ var reactivateEnrollment;
124145
124205
  var listAssessments;
124146
124206
  var createAssessment;
124147
124207
  var deleteAssessment;
@@ -124556,17 +124616,6 @@ var init_timeback_controller = __esm(() => {
124556
124616
  });
124557
124617
  return ctx.services.timebackAdmin.adjustMasteredUnits(body2, ctx.user);
124558
124618
  });
124559
- toggleCompletion = requireGameManagementAccess(async (ctx) => {
124560
- const body2 = await parseRequestBody(ctx.request, ToggleCourseCompletionRequestSchema);
124561
- logger65.debug("Toggling course completion", {
124562
- requesterId: ctx.user.id,
124563
- gameId: body2.gameId,
124564
- courseId: body2.courseId,
124565
- studentId: body2.studentId,
124566
- action: body2.action
124567
- });
124568
- return ctx.services.timebackAdmin.toggleCourseCompletion(body2, ctx.user);
124569
- });
124570
124619
  searchStudents = requireGameManagementAccess(async (ctx) => {
124571
124620
  const gameId = ctx.params.gameId;
124572
124621
  const courseId = ctx.params.courseId;
@@ -124602,6 +124651,17 @@ var init_timeback_controller = __esm(() => {
124602
124651
  });
124603
124652
  return ctx.services.timebackAdmin.unenrollStudent(body2, ctx.user);
124604
124653
  });
124654
+ reactivateEnrollment = requireGameManagementAccess(async (ctx) => {
124655
+ const body2 = await parseRequestBody(ctx.request, ReactivateEnrollmentRequestSchema);
124656
+ logger65.debug("Reactivating enrollment", {
124657
+ requesterId: ctx.user.id,
124658
+ gameId: body2.gameId,
124659
+ courseId: body2.courseId,
124660
+ studentId: body2.studentId,
124661
+ enrollmentId: body2.enrollmentId
124662
+ });
124663
+ return ctx.services.timebackAdmin.reactivateEnrollment(body2, ctx.user);
124664
+ });
124605
124665
  listAssessments = requireGameManagementAccess(async (ctx) => {
124606
124666
  const { gameId, courseId } = ctx.params;
124607
124667
  if (!gameId || !courseId) {
@@ -124747,10 +124807,10 @@ var init_timeback_controller = __esm(() => {
124747
124807
  grantXp,
124748
124808
  adjustTime,
124749
124809
  adjustMastery,
124750
- toggleCompletion,
124751
124810
  searchStudents,
124752
124811
  enrollStudent,
124753
124812
  unenrollStudent,
124813
+ reactivateEnrollment,
124754
124814
  listAssessments,
124755
124815
  createAssessment,
124756
124816
  deleteAssessment,
@@ -127584,7 +127644,7 @@ var import_picocolors12 = __toESM(require_picocolors(), 1);
127584
127644
  // package.json
127585
127645
  var package_default2 = {
127586
127646
  name: "@playcademy/vite-plugin",
127587
- version: "0.2.29",
127647
+ version: "0.2.30-beta.2",
127588
127648
  type: "module",
127589
127649
  exports: {
127590
127650
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "0.2.29",
3
+ "version": "0.2.30-beta.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {