@superapp_men/submit-assessment-results 1.0.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/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # @superapp_men/submit-assessment-results
2
+
3
+ Package for **partner applications** (running inside the SuperApp iframe or Capacitor WebView) to submit assessment results to the SuperApp. The SuperApp forwards the payload to the `SubmitAssessmentResults` backend endpoint (adding `academicYearId` and `periodWeekSubjectId`) and returns the **exact backend response** to the partner.
4
+
5
+ ## Features
6
+
7
+ - **Builder / factory** to construct skills and questions data in a type-safe way.
8
+ - **Client-side validation** before calling the SuperApp: the same rules as the backend `SubmitAssessmentResultsValidator` run first. Invalid payloads get immediate `400`-style errors with field-level messages without a round-trip.
9
+ - **No `academicYearId` or `periodWeekSubjectId`** in the partner payload — the SuperApp adds them when calling the API.
10
+ - **Exact backend response** returned: success (200), validation errors (400), functional errors (422), and technical errors (404, 409, 429, 500).
11
+
12
+ **Current limitation (upcoming feature):** `subSkills` must be `null` or an empty array `[]`. Sub-skills are not yet supported; each skill must use `questions` only. Support for nested sub-skills will be added in a future release.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @superapp_men/submit-assessment-results
18
+ ```
19
+
20
+ ## Usage (Partner App)
21
+
22
+ Each skill must contain **questions** (not sub-skills). The `subSkills` property must be omitted, `null`, or an empty array `[]`; non-empty `subSkills` will fail validation until the feature is released.
23
+
24
+ ### 1. Build payload with the builder
25
+
26
+ ```ts
27
+ import {
28
+ AssessmentSubmissionClient,
29
+ AssessmentSubmission,
30
+ QuestionRole,
31
+ } from "@superapp_men/submit-assessment-results";
32
+
33
+ const skill1 = AssessmentSubmission.skill("MATH_ADD_01", 1, true)
34
+ .questions([
35
+ AssessmentSubmission.question("Q001", true, QuestionRole.VALIDATION)
36
+ .order(1)
37
+ .responseTime(5000)
38
+ .build(),
39
+ AssessmentSubmission.question("Q002", false, QuestionRole.VALIDATION)
40
+ .order(2)
41
+ .build(),
42
+ ])
43
+ .build();
44
+
45
+ const payload = AssessmentSubmission.builder()
46
+ .setStudentId("3fa85f64-5717-4562-b3fc-2c963f66afa6")
47
+ .setPartnerCode("PARTNER001")
48
+ .setAttemptId("unique-attempt-id-12345")
49
+ .setAser(false)
50
+ .setTotalSkillsInWeek(10)
51
+ .addSkill(skill1)
52
+ .build();
53
+ ```
54
+
55
+ ### 2. Adding multiple skills (e.g. 3 skills)
56
+
57
+ Use **`addSkill()`** for each skill or **`addSkills()`** with an array.
58
+
59
+ **Option A — One `addSkill()` per skill:**
60
+
61
+ ```ts
62
+ const skill1 = AssessmentSubmission.skill("MATH_ADD_01", 1, true)
63
+ .questions([
64
+ AssessmentSubmission.question("Q001", true, QuestionRole.VALIDATION).order(1).build(),
65
+ AssessmentSubmission.question("Q002", false, QuestionRole.VALIDATION).order(2).build(),
66
+ ])
67
+ .build();
68
+
69
+ const skill2 = AssessmentSubmission.skill("MATH_SUB_01", 2, true)
70
+ .questions([
71
+ AssessmentSubmission.question("Q003", true, QuestionRole.VALIDATION).order(1).build(),
72
+ AssessmentSubmission.question("Q004", true, QuestionRole.VALIDATION).order(2).build(),
73
+ ])
74
+ .build();
75
+
76
+ const skill3 = AssessmentSubmission.skill("MATH_MUL_01", 3, false)
77
+ .questions([
78
+ AssessmentSubmission.question("Q005", false, QuestionRole.VALIDATION).order(1).build(),
79
+ ])
80
+ .build();
81
+
82
+ const payload = AssessmentSubmission.builder()
83
+ .setStudentId(studentId)
84
+ .setPartnerCode("PARTNER001")
85
+ .setAttemptId("attempt-" + Date.now())
86
+ .setTotalSkillsInWeek(3)
87
+ .addSkill(skill1)
88
+ .addSkill(skill2)
89
+ .addSkill(skill3)
90
+ .build();
91
+ ```
92
+
93
+ **Option B — Build an array and use `addSkills()`:**
94
+
95
+ ```ts
96
+ const skills = [
97
+ AssessmentSubmission.skill("MATH_ADD_01", 1, true)
98
+ .questions([
99
+ AssessmentSubmission.question("Q001", true, QuestionRole.VALIDATION).order(1).build(),
100
+ AssessmentSubmission.question("Q002", false, QuestionRole.VALIDATION).order(2).build(),
101
+ ])
102
+ .build(),
103
+ AssessmentSubmission.skill("MATH_SUB_01", 2, true)
104
+ .questions([
105
+ AssessmentSubmission.question("Q003", true, QuestionRole.VALIDATION).order(1).build(),
106
+ ])
107
+ .build(),
108
+ AssessmentSubmission.skill("MATH_MUL_01", 3, false)
109
+ .questions([
110
+ AssessmentSubmission.question("Q005", false, QuestionRole.VALIDATION).order(1).build(),
111
+ ])
112
+ .build(),
113
+ ];
114
+
115
+ const payload = AssessmentSubmission.builder()
116
+ .setStudentId(studentId)
117
+ .setPartnerCode("PARTNER001")
118
+ .setAttemptId("attempt-" + Date.now())
119
+ .setTotalSkillsInWeek(3)
120
+ .addSkills(skills)
121
+ .build();
122
+ ```
123
+
124
+ Make sure **`setTotalSkillsInWeek(n)`** matches the number of skills you submit (e.g. `3` when you add 3 skills).
125
+
126
+ ### 3. Submit and handle response
127
+
128
+ `client.submit(payload)` runs **client-side validation** first (same rules as the backend). If validation fails, it returns immediately with `ok: false`, `statusCode: 400`, and `body.errors` (field paths and messages). If validation passes, it sends the payload to the SuperApp and returns the backend response.
129
+
130
+ ```ts
131
+ const client = new AssessmentSubmissionClient({ timeout: 30000, debug: true });
132
+
133
+ const result = await client.submit(payload);
134
+
135
+ if (result.ok) {
136
+ console.log("Success:", result.body.attemptId, result.body.status);
137
+ // result.body is SubmitAssessmentResultsResponse
138
+ } else {
139
+ console.log("Error:", result.statusCode, result.body);
140
+ // result.body is ValidationProblemDetails (400) or ProblemDetails (404/409/422/429/500)
141
+ if (result.statusCode === 400 && "errors" in result.body) {
142
+ console.log("Validation errors:", result.body.errors);
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### 4. Optional: validate without submitting
148
+
149
+ You can run the same validation without sending to the SuperApp (e.g. to show errors in the UI before submit):
150
+
151
+ ```ts
152
+ import { validateSubmitAssessmentPayload, toValidationProblemDetails } from "@superapp_men/submit-assessment-results";
153
+
154
+ const validation = validateSubmitAssessmentPayload(payload);
155
+ if (!validation.valid) {
156
+ const details = toValidationProblemDetails(validation.errors);
157
+ console.log("Validation errors:", details.errors); // Record<path, string[]>
158
+ }
159
+ ```
160
+
161
+ ## API
162
+
163
+ - **`AssessmentSubmission.builder()`** — Fluent builder for the full partner payload.
164
+ - **`AssessmentSubmission.skill(code, order, passed)`** — Builder for one skill. Use `.questions(...)` only; `.subSkills(...)` with a non-empty array is not yet supported (validation will fail).
165
+ - **`AssessmentSubmission.question(code, isCorrect, questionRole)`** — Builder for one question.
166
+ - **`new AssessmentSubmissionClient(config)`** — Client with `submit(payload)` returning `Promise<SubmitAssessmentResultsApiResult>`. Runs client-side validation before sending.
167
+ - **`validateSubmitAssessmentPayload(payload)`** — Returns `{ valid, errors }` using the same rules as the backend validator.
168
+ - **`toValidationProblemDetails(errors)`** — Converts `ValidationError[]` to a backend-style `{ status: 400, title, errors }` object.
169
+ - **`SubmitAssessmentResultsApiResult`** — `{ ok: true, statusCode, body }` or `{ ok: false, statusCode, body }` with the exact backend response (or client-side validation errors when invalid).
170
+
171
+ ## SuperApp integration
172
+
173
+ The SuperApp must:
174
+
175
+ 1. Listen for messages with `type: "assessment:submit-request"`.
176
+ 2. Enrich the payload with `academicYearId` and `periodWeekSubjectId`.
177
+ 3. Call `POST api/v1.0/submit-results` with the enriched body and forward the HTTP status and response body back to the partner via postMessage with `type: "assessment:submit-response"`.
178
+
179
+ See the main SuperApp repo for `IframeCommunicationHandler` and the service that calls the SubmitAssessmentResults endpoint.
180
+
181
+ ## Types (superapp entry)
182
+
183
+ For SuperApp-side code that handles these messages:
184
+
185
+ ```ts
186
+ import type {
187
+ AssessmentSubmitRequestMessage,
188
+ AssessmentSubmitResponseMessage,
189
+ SubmitAssessmentResultsPartnerPayload,
190
+ } from "@superapp_men/submit-assessment-results/superapp";
191
+ import { AssessmentMessageType } from "@superapp_men/submit-assessment-results/superapp";
192
+ ```
193
+
194
+ ## License
195
+
196
+ MIT
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Types for @superapp_men/submit-assessment-results
3
+ * Aligned with SubmitAssessmentResults API (academicYearId and periodWeekSubjectId
4
+ * are omitted in partner payload; SuperApp adds them when calling the endpoint).
5
+ */
6
+ /** Question role in the assessment (matches backend enum). */
7
+ declare enum QuestionRole {
8
+ POSITIONING = 1,
9
+ VALIDATION = 2,
10
+ REMEDIATION = 3,
11
+ BONUS = 4
12
+ }
13
+ /** Response time: milliseconds (number) or "hh:mm:ss" string. */
14
+ type ResponseTimeInput = number | string;
15
+ /** Single question result (partner payload). */
16
+ interface SubmittedQuestionResultPayload {
17
+ questionCode: string;
18
+ isCorrect: boolean;
19
+ questionOrder: number;
20
+ questionRole: QuestionRole;
21
+ responseTime?: ResponseTimeInput;
22
+ attemptsCount?: number;
23
+ questionTextFr?: string;
24
+ questionTextAr?: string;
25
+ }
26
+ /** Single skill result; must have either subSkills or questions. */
27
+ interface SubmittedSkillResultPayload {
28
+ skillCode: string;
29
+ skillOrder: number;
30
+ passed: boolean;
31
+ titleFr?: string;
32
+ titleAr?: string;
33
+ descriptionFr?: string;
34
+ descriptionAr?: string;
35
+ subSkills?: SubmittedSkillResultPayload[];
36
+ questions?: SubmittedQuestionResultPayload[];
37
+ }
38
+ /**
39
+ * Partner payload for submit assessment results.
40
+ * academicYearId and periodWeekSubjectId are NOT included; the SuperApp
41
+ * adds them when calling the backend.
42
+ */
43
+ interface SubmitAssessmentResultsPartnerPayload {
44
+ studentId: string;
45
+ partnerCode: string;
46
+ attemptId: string;
47
+ isAser?: boolean;
48
+ totalSkillsInWeek: number;
49
+ skills: SubmittedSkillResultPayload[];
50
+ }
51
+ /** Success response from SubmitAssessmentResults endpoint. */
52
+ interface SubmitAssessmentResultsResponse {
53
+ attemptId: string;
54
+ isDuplicate: boolean;
55
+ status: string;
56
+ message: string | null;
57
+ serverTimeUtc: string;
58
+ }
59
+ /** ValidationProblemDetails (400). */
60
+ interface ValidationProblemDetails {
61
+ type?: string;
62
+ title?: string;
63
+ status: number;
64
+ detail?: string;
65
+ instance?: string;
66
+ errors?: Record<string, string[]>;
67
+ }
68
+ /** ProblemDetails (404, 409, 422, 429, 500). */
69
+ interface ProblemDetails {
70
+ type?: string;
71
+ title?: string;
72
+ status?: number;
73
+ detail?: string;
74
+ instance?: string;
75
+ [key: string]: unknown;
76
+ }
77
+ /**
78
+ * Result of calling submit: either the success response or an error response
79
+ * with statusCode and body as returned by the backend.
80
+ */
81
+ type SubmitAssessmentResultsApiResult = {
82
+ ok: true;
83
+ statusCode: number;
84
+ body: SubmitAssessmentResultsResponse;
85
+ } | {
86
+ ok: false;
87
+ statusCode: number;
88
+ body: ValidationProblemDetails | ProblemDetails;
89
+ };
90
+ /** Message types for iframe/Capacitor communication. */
91
+ declare enum AssessmentMessageType {
92
+ REQUEST = "assessment:submit-request",
93
+ RESPONSE = "assessment:submit-response"
94
+ }
95
+
96
+ interface AssessmentSubmissionClientConfig {
97
+ /** Request timeout in ms (default 30000). */
98
+ timeout?: number;
99
+ /** Enable debug logs (default false). */
100
+ debug?: boolean;
101
+ }
102
+ /**
103
+ * Client for partner applications to submit assessment results to the SuperApp.
104
+ * Sends the payload via postMessage to the parent (SuperApp); the SuperApp
105
+ * adds academicYearId and periodWeekSubjectId and calls the backend, then
106
+ * returns the exact backend response (success, validation errors, functional
107
+ * errors, or technical errors).
108
+ */
109
+ declare class AssessmentSubmissionClient {
110
+ private bridge;
111
+ private config;
112
+ constructor(config?: AssessmentSubmissionClientConfig);
113
+ /**
114
+ * Submit assessment results. Runs client-side validation first (same rules as backend);
115
+ * if invalid, returns immediately with statusCode 400 and errors. Otherwise sends to
116
+ * SuperApp and returns the exact backend response.
117
+ */
118
+ submit(payload: SubmitAssessmentResultsPartnerPayload): Promise<SubmitAssessmentResultsApiResult>;
119
+ setDebug(enabled: boolean): void;
120
+ destroy(): void;
121
+ }
122
+
123
+ /**
124
+ * Fluent builder for a single question result.
125
+ */
126
+ declare class QuestionResultBuilder {
127
+ private data;
128
+ constructor(questionCode: string, isCorrect: boolean, questionRole: QuestionRole);
129
+ order(questionOrder: number): this;
130
+ responseTime(value: number | string): this;
131
+ attemptsCount(count: number): this;
132
+ questionText(fr?: string, ar?: string): this;
133
+ build(): SubmittedQuestionResultPayload;
134
+ }
135
+ /**
136
+ * Fluent builder for a single skill result (with questions or sub-skills).
137
+ */
138
+ declare class SkillResultBuilder {
139
+ private data;
140
+ constructor(skillCode: string, skillOrder: number, passed: boolean);
141
+ title(titleFr?: string, titleAr?: string): this;
142
+ description(descriptionFr?: string, descriptionAr?: string): this;
143
+ questions(questions: SubmittedQuestionResultPayload[]): this;
144
+ subSkills(subSkills: SubmittedSkillResultPayload[]): this;
145
+ build(): SubmittedSkillResultPayload;
146
+ }
147
+ /**
148
+ * Fluent builder for the full partner payload (no academicYearId / periodWeekSubjectId).
149
+ */
150
+ declare class AssessmentSubmissionBuilder {
151
+ private studentId;
152
+ private partnerCode;
153
+ private attemptId;
154
+ private isAser;
155
+ private totalSkillsInWeek;
156
+ private skills;
157
+ setStudentId(studentId: string): this;
158
+ setPartnerCode(partnerCode: string): this;
159
+ setAttemptId(attemptId: string): this;
160
+ setAser(isAser: boolean): this;
161
+ setTotalSkillsInWeek(total: number): this;
162
+ addSkill(skill: SubmittedSkillResultPayload): this;
163
+ addSkills(skillList: SubmittedSkillResultPayload[]): this;
164
+ build(): SubmitAssessmentResultsPartnerPayload;
165
+ }
166
+ /** Factory helpers. */
167
+ declare const AssessmentSubmission: {
168
+ builder(): AssessmentSubmissionBuilder;
169
+ skill(skillCode: string, skillOrder: number, passed: boolean): SkillResultBuilder;
170
+ question(questionCode: string, isCorrect: boolean, questionRole?: QuestionRole): QuestionResultBuilder;
171
+ };
172
+
173
+ /**
174
+ * Client-side validation mirroring backend SubmitAssessmentResultsValidator.
175
+ * Run before sending to SuperApp so partners get immediate validation errors
176
+ * without a round-trip to the API.
177
+ */
178
+
179
+ interface ValidationError {
180
+ /** Property path (e.g. "StudentId", "Skills[0].SkillCode"). */
181
+ key: string;
182
+ message: string;
183
+ }
184
+ interface ValidationResult {
185
+ valid: boolean;
186
+ errors: ValidationError[];
187
+ }
188
+ /** Flatten to backend-style errors record (key -> string[]). */
189
+ declare function toValidationProblemDetails(errors: ValidationError[]): {
190
+ status: number;
191
+ title: string;
192
+ errors: Record<string, string[]>;
193
+ };
194
+ /**
195
+ * Validate the partner payload using the same rules as the backend
196
+ * SubmitAssessmentResultsValidator. Call this before submit() to get
197
+ * immediate validation errors without calling the SuperApp/backend.
198
+ */
199
+ declare function validateSubmitAssessmentPayload(payload: SubmitAssessmentResultsPartnerPayload): ValidationResult;
200
+
201
+ export { AssessmentMessageType, AssessmentSubmission, AssessmentSubmissionBuilder, AssessmentSubmissionClient, QuestionResultBuilder, QuestionRole, SkillResultBuilder, toValidationProblemDetails, validateSubmitAssessmentPayload };
202
+ export type { AssessmentSubmissionClientConfig, ProblemDetails, ResponseTimeInput, SubmitAssessmentResultsApiResult, SubmitAssessmentResultsPartnerPayload, SubmitAssessmentResultsResponse, SubmittedQuestionResultPayload, SubmittedSkillResultPayload, ValidationError, ValidationProblemDetails, ValidationResult };