Add LMS personas catalog and CRUD API.
Introduce lms_personas table, repoint practice persona_id FKs off users, validate persona refs on LMS and exam-prep practice flows, personas.* RBAC permissions, and OpenAPI docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
71bc09a638
commit
873be1b482
|
|
@ -27,6 +27,7 @@ import (
|
||||||
minioservice "Yimaru-Backend/internal/services/minio"
|
minioservice "Yimaru-Backend/internal/services/minio"
|
||||||
moduleservice "Yimaru-Backend/internal/services/modules"
|
moduleservice "Yimaru-Backend/internal/services/modules"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
|
personasservice "Yimaru-Backend/internal/services/personas"
|
||||||
practicesservice "Yimaru-Backend/internal/services/practices"
|
practicesservice "Yimaru-Backend/internal/services/practices"
|
||||||
programsservice "Yimaru-Backend/internal/services/programs"
|
programsservice "Yimaru-Backend/internal/services/programs"
|
||||||
"Yimaru-Backend/internal/services/questions"
|
"Yimaru-Backend/internal/services/questions"
|
||||||
|
|
@ -395,6 +396,7 @@ func main() {
|
||||||
// Questions service (unified questions system)
|
// Questions service (unified questions system)
|
||||||
questionsSvc := questions.NewService(store)
|
questionsSvc := questions.NewService(store)
|
||||||
faqSvc := faqs.NewService(repository.NewFAQStore(store))
|
faqSvc := faqs.NewService(repository.NewFAQStore(store))
|
||||||
|
personasSvc := personasservice.NewService(store)
|
||||||
examPrepSvc := examprep.NewService(store)
|
examPrepSvc := examprep.NewService(store)
|
||||||
|
|
||||||
// LMS programs (top-level hierarchy)
|
// LMS programs (top-level hierarchy)
|
||||||
|
|
@ -456,6 +458,7 @@ func main() {
|
||||||
assessmentSvc,
|
assessmentSvc,
|
||||||
questionsSvc,
|
questionsSvc,
|
||||||
faqSvc,
|
faqSvc,
|
||||||
|
personasSvc,
|
||||||
examPrepSvc,
|
examPrepSvc,
|
||||||
programSvc,
|
programSvc,
|
||||||
courseSvc,
|
courseSvc,
|
||||||
|
|
|
||||||
17
db/migrations/000063_lms_personas.down.sql
Normal file
17
db/migrations/000063_lms_personas.down.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
ALTER TABLE exam_prep.lesson_practices DROP CONSTRAINT IF EXISTS lesson_practices_persona_id_fkey;
|
||||||
|
|
||||||
|
UPDATE exam_prep.lesson_practices
|
||||||
|
SET persona_id = NULL;
|
||||||
|
|
||||||
|
ALTER TABLE exam_prep.lesson_practices
|
||||||
|
ADD CONSTRAINT lesson_practices_persona_id_fkey FOREIGN KEY (persona_id) REFERENCES users (id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
ALTER TABLE lms_practices DROP CONSTRAINT IF EXISTS lms_practices_persona_id_fkey;
|
||||||
|
|
||||||
|
UPDATE lms_practices
|
||||||
|
SET persona_id = NULL;
|
||||||
|
|
||||||
|
ALTER TABLE lms_practices
|
||||||
|
ADD CONSTRAINT lms_practices_persona_id_fkey FOREIGN KEY (persona_id) REFERENCES users (id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS lms_personas;
|
||||||
34
db/migrations/000063_lms_personas.up.sql
Normal file
34
db/migrations/000063_lms_personas.up.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
-- Catalog of LMS personas (coach/avatar profiles) referenced by Learn English + exam-prep practices.
|
||||||
|
CREATE TABLE lms_personas (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
avatar_url TEXT,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_personas_is_active ON lms_personas (is_active)
|
||||||
|
WHERE is_active;
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_personas_created_at ON lms_personas (created_at DESC);
|
||||||
|
|
||||||
|
-- persona_id historically referenced users.id; personas are now catalog rows on lms_personas.
|
||||||
|
ALTER TABLE lms_practices DROP CONSTRAINT IF EXISTS lms_practices_persona_id_fkey;
|
||||||
|
|
||||||
|
UPDATE lms_practices
|
||||||
|
SET persona_id = NULL
|
||||||
|
WHERE persona_id IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE lms_practices
|
||||||
|
ADD CONSTRAINT lms_practices_persona_id_fkey FOREIGN KEY (persona_id) REFERENCES lms_personas (id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
ALTER TABLE exam_prep.lesson_practices DROP CONSTRAINT IF EXISTS lesson_practices_persona_id_fkey;
|
||||||
|
|
||||||
|
UPDATE exam_prep.lesson_practices
|
||||||
|
SET persona_id = NULL
|
||||||
|
WHERE persona_id IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE exam_prep.lesson_practices
|
||||||
|
ADD CONSTRAINT lesson_practices_persona_id_fkey FOREIGN KEY (persona_id) REFERENCES lms_personas (id) ON DELETE SET NULL;
|
||||||
42
db/query/lms_personas.sql
Normal file
42
db/query/lms_personas.sql
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
-- name: CreateLmsPersona :one
|
||||||
|
INSERT INTO lms_personas (name, description, avatar_url, is_active)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetLmsPersonaByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM lms_personas
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: UpdateLmsPersona :one
|
||||||
|
UPDATE lms_personas
|
||||||
|
SET
|
||||||
|
name = COALESCE(sqlc.narg('name')::varchar, name),
|
||||||
|
description = COALESCE(sqlc.narg('description')::text, description),
|
||||||
|
avatar_url = COALESCE(sqlc.narg('avatar_url')::text, avatar_url),
|
||||||
|
is_active = COALESCE(sqlc.narg('is_active')::boolean, is_active),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = sqlc.arg('id')
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteLmsPersona :exec
|
||||||
|
DELETE FROM lms_personas
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListLmsPersonas :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.avatar_url,
|
||||||
|
p.is_active,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM lms_personas p
|
||||||
|
WHERE (
|
||||||
|
sqlc.arg('filter_active')::boolean = FALSE
|
||||||
|
OR p.is_active = TRUE
|
||||||
|
)
|
||||||
|
ORDER BY p.name ASC, p.created_at DESC
|
||||||
|
LIMIT $1 OFFSET $2;
|
||||||
154
docs/docs.go
154
docs/docs.go
|
|
@ -3963,6 +3963,123 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/personas": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"personas"
|
||||||
|
],
|
||||||
|
"summary": "List LMS personas (catalog for practice assignment)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "When true (default), return only active personas",
|
||||||
|
"name": "active_only",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page size",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Offset",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"personas"
|
||||||
|
],
|
||||||
|
"summary": "Create LMS persona catalog entry",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Persona",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.CreateLmsPersonaInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/personas/{id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"personas"
|
||||||
|
],
|
||||||
|
"summary": "Get LMS persona by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Persona ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"personas"
|
||||||
|
],
|
||||||
|
"summary": "Update LMS persona",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Persona ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Fields to update",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.UpdateLmsPersonaInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"personas"
|
||||||
|
],
|
||||||
|
"summary": "Delete LMS persona (practices referencing it will have persona_id cleared)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Persona ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/practices": {
|
"/api/v1/practices": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
|
@ -10518,6 +10635,26 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.CreateLmsPersonaInput": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"avatar_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.CreateModuleInput": {
|
"domain.CreateModuleInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11441,6 +11578,23 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.UpdateLmsPersonaInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"avatar_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.UpdateModuleInput": {
|
"domain.UpdateModuleInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -3955,6 +3955,123 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/personas": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"personas"
|
||||||
|
],
|
||||||
|
"summary": "List LMS personas (catalog for practice assignment)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "When true (default), return only active personas",
|
||||||
|
"name": "active_only",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page size",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Offset",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"personas"
|
||||||
|
],
|
||||||
|
"summary": "Create LMS persona catalog entry",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Persona",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.CreateLmsPersonaInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/personas/{id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"personas"
|
||||||
|
],
|
||||||
|
"summary": "Get LMS persona by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Persona ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"personas"
|
||||||
|
],
|
||||||
|
"summary": "Update LMS persona",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Persona ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Fields to update",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.UpdateLmsPersonaInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"personas"
|
||||||
|
],
|
||||||
|
"summary": "Delete LMS persona (practices referencing it will have persona_id cleared)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Persona ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/practices": {
|
"/api/v1/practices": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
|
@ -10510,6 +10627,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.CreateLmsPersonaInput": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"avatar_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.CreateModuleInput": {
|
"domain.CreateModuleInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11433,6 +11570,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.UpdateLmsPersonaInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"avatar_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.UpdateModuleInput": {
|
"domain.UpdateModuleInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -447,6 +447,19 @@ definitions:
|
||||||
required:
|
required:
|
||||||
- title
|
- title
|
||||||
type: object
|
type: object
|
||||||
|
domain.CreateLmsPersonaInput:
|
||||||
|
properties:
|
||||||
|
avatar_url:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
domain.CreateModuleInput:
|
domain.CreateModuleInput:
|
||||||
properties:
|
properties:
|
||||||
description:
|
description:
|
||||||
|
|
@ -1081,6 +1094,17 @@ definitions:
|
||||||
video_url:
|
video_url:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
domain.UpdateLmsPersonaInput:
|
||||||
|
properties:
|
||||||
|
avatar_url:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
is_active:
|
||||||
|
type: boolean
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.UpdateModuleInput:
|
domain.UpdateModuleInput:
|
||||||
properties:
|
properties:
|
||||||
description:
|
description:
|
||||||
|
|
@ -5168,6 +5192,84 @@ paths:
|
||||||
summary: Handle ArifPay webhook
|
summary: Handle ArifPay webhook
|
||||||
tags:
|
tags:
|
||||||
- payments
|
- payments
|
||||||
|
/api/v1/personas:
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- default: true
|
||||||
|
description: When true (default), return only active personas
|
||||||
|
in: query
|
||||||
|
name: active_only
|
||||||
|
type: boolean
|
||||||
|
- description: Page size
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
- description: Offset
|
||||||
|
in: query
|
||||||
|
name: offset
|
||||||
|
type: integer
|
||||||
|
responses: {}
|
||||||
|
summary: List LMS personas (catalog for practice assignment)
|
||||||
|
tags:
|
||||||
|
- personas
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Persona
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.CreateLmsPersonaInput'
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.Response'
|
||||||
|
summary: Create LMS persona catalog entry
|
||||||
|
tags:
|
||||||
|
- personas
|
||||||
|
/api/v1/personas/{id}:
|
||||||
|
delete:
|
||||||
|
parameters:
|
||||||
|
- description: Persona ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
responses: {}
|
||||||
|
summary: Delete LMS persona (practices referencing it will have persona_id cleared)
|
||||||
|
tags:
|
||||||
|
- personas
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: Persona ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
responses: {}
|
||||||
|
summary: Get LMS persona by ID
|
||||||
|
tags:
|
||||||
|
- personas
|
||||||
|
put:
|
||||||
|
parameters:
|
||||||
|
- description: Persona ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Fields to update
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.UpdateLmsPersonaInput'
|
||||||
|
responses: {}
|
||||||
|
summary: Update LMS persona
|
||||||
|
tags:
|
||||||
|
- personas
|
||||||
/api/v1/practices:
|
/api/v1/practices:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
182
gen/db/lms_personas.sql.go
Normal file
182
gen/db/lms_personas.sql.go
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_personas.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateLmsPersona = `-- name: CreateLmsPersona :one
|
||||||
|
INSERT INTO lms_personas (name, description, avatar_url, is_active)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, name, description, avatar_url, is_active, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateLmsPersonaParams struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateLmsPersona(ctx context.Context, arg CreateLmsPersonaParams) (LmsPersona, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateLmsPersona,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.AvatarUrl,
|
||||||
|
arg.IsActive,
|
||||||
|
)
|
||||||
|
var i LmsPersona
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.AvatarUrl,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteLmsPersona = `-- name: DeleteLmsPersona :exec
|
||||||
|
DELETE FROM lms_personas
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteLmsPersona(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteLmsPersona, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetLmsPersonaByID = `-- name: GetLmsPersonaByID :one
|
||||||
|
SELECT id, name, description, avatar_url, is_active, created_at, updated_at
|
||||||
|
FROM lms_personas
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetLmsPersonaByID(ctx context.Context, id int64) (LmsPersona, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetLmsPersonaByID, id)
|
||||||
|
var i LmsPersona
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.AvatarUrl,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLmsPersonas = `-- name: ListLmsPersonas :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.avatar_url,
|
||||||
|
p.is_active,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM lms_personas p
|
||||||
|
WHERE (
|
||||||
|
$3::boolean = FALSE
|
||||||
|
OR p.is_active = TRUE
|
||||||
|
)
|
||||||
|
ORDER BY p.name ASC, p.created_at DESC
|
||||||
|
LIMIT $1 OFFSET $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListLmsPersonasParams struct {
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
FilterActive bool `json:"filter_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLmsPersonasRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListLmsPersonas(ctx context.Context, arg ListLmsPersonasParams) ([]ListLmsPersonasRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLmsPersonas, arg.Limit, arg.Offset, arg.FilterActive)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListLmsPersonasRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListLmsPersonasRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.AvatarUrl,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateLmsPersona = `-- name: UpdateLmsPersona :one
|
||||||
|
UPDATE lms_personas
|
||||||
|
SET
|
||||||
|
name = COALESCE($1::varchar, name),
|
||||||
|
description = COALESCE($2::text, description),
|
||||||
|
avatar_url = COALESCE($3::text, avatar_url),
|
||||||
|
is_active = COALESCE($4::boolean, is_active),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $5
|
||||||
|
RETURNING id, name, description, avatar_url, is_active, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateLmsPersonaParams struct {
|
||||||
|
Name pgtype.Text `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||||
|
IsActive pgtype.Bool `json:"is_active"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateLmsPersona(ctx context.Context, arg UpdateLmsPersonaParams) (LmsPersona, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateLmsPersona,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.AvatarUrl,
|
||||||
|
arg.IsActive,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i LmsPersona
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.AvatarUrl,
|
||||||
|
&i.IsActive,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
@ -138,6 +138,16 @@ type LevelToSubCourse struct {
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
SubCourseID int64 `json:"sub_course_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LmsPersona struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type LmsPractice struct {
|
type LmsPractice struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
CourseID pgtype.Int8 `json:"course_id"`
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ type ExamPrepPractice struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
StoryDescription *string `json:"story_description,omitempty"`
|
StoryDescription *string `json:"story_description,omitempty"`
|
||||||
StoryImage *string `json:"story_image,omitempty"`
|
StoryImage *string `json:"story_image,omitempty"`
|
||||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
PersonaID *int64 `json:"persona_id,omitempty"` // lms_personas.id when set
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
PublishStatus PracticePublishStatus `json:"publish_status"`
|
PublishStatus PracticePublishStatus `json:"publish_status"`
|
||||||
QuickTips *string `json:"quick_tips,omitempty"`
|
QuickTips *string `json:"quick_tips,omitempty"`
|
||||||
|
|
|
||||||
34
internal/domain/lms_persona.go
Normal file
34
internal/domain/lms_persona.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrPersonaNotFound is returned when an lms_personas row does not exist.
|
||||||
|
var ErrPersonaNotFound = errors.New("persona not found")
|
||||||
|
|
||||||
|
// LmsPersona is a coach / character profile stored in lms_personas and referenced by practice shells.
|
||||||
|
type LmsPersona struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
AvatarURL *string `json:"avatar_url,omitempty"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateLmsPersonaInput struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
AvatarURL *string `json:"avatar_url,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateLmsPersonaInput struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
AvatarURL *string `json:"avatar_url,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ type Practice struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
StoryDescription *string `json:"story_description,omitempty"`
|
StoryDescription *string `json:"story_description,omitempty"`
|
||||||
StoryImage *string `json:"story_image,omitempty"`
|
StoryImage *string `json:"story_image,omitempty"`
|
||||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
PersonaID *int64 `json:"persona_id,omitempty"` // lms_personas.id when set
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
PublishStatus PracticePublishStatus `json:"publish_status"`
|
PublishStatus PracticePublishStatus `json:"publish_status"`
|
||||||
QuickTips *string `json:"quick_tips,omitempty"`
|
QuickTips *string `json:"quick_tips,omitempty"`
|
||||||
|
|
|
||||||
21
internal/ports/lms_persona.go
Normal file
21
internal/ports/lms_persona.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LmsPersonaReader resolves catalog personas referenced by LMS / exam-prep practices.
|
||||||
|
type LmsPersonaReader interface {
|
||||||
|
GetLmsPersonaByID(ctx context.Context, id int64) (domain.LmsPersona, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LmsPersonaStore is full CRUD for lms_personas.
|
||||||
|
type LmsPersonaStore interface {
|
||||||
|
LmsPersonaReader
|
||||||
|
CreateLmsPersona(ctx context.Context, in domain.CreateLmsPersonaInput) (domain.LmsPersona, error)
|
||||||
|
UpdateLmsPersona(ctx context.Context, id int64, in domain.UpdateLmsPersonaInput) (domain.LmsPersona, error)
|
||||||
|
DeleteLmsPersona(ctx context.Context, id int64) error
|
||||||
|
ListLmsPersonas(ctx context.Context, activeOnly bool, limit, offset int32) ([]domain.LmsPersona, int64, error)
|
||||||
|
}
|
||||||
115
internal/repository/lms_personas.go
Normal file
115
internal/repository/lms_personas.go
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lmsPersonaToDomain(p dbgen.LmsPersona) domain.LmsPersona {
|
||||||
|
out := domain.LmsPersona{
|
||||||
|
ID: p.ID,
|
||||||
|
Name: p.Name,
|
||||||
|
IsActive: p.IsActive,
|
||||||
|
}
|
||||||
|
out.Description = fromPgText(p.Description)
|
||||||
|
out.AvatarURL = fromPgText(p.AvatarUrl)
|
||||||
|
out.CreatedAt = p.CreatedAt.Time
|
||||||
|
if p.UpdatedAt.Valid {
|
||||||
|
t := p.UpdatedAt.Time
|
||||||
|
out.UpdatedAt = &t
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionalBoolUpdatePB(v *bool) pgtype.Bool {
|
||||||
|
if v == nil {
|
||||||
|
return pgtype.Bool{Valid: false}
|
||||||
|
}
|
||||||
|
return pgtype.Bool{Bool: *v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateLmsPersona(ctx context.Context, in domain.CreateLmsPersonaInput) (domain.LmsPersona, error) {
|
||||||
|
active := true
|
||||||
|
if in.IsActive != nil {
|
||||||
|
active = *in.IsActive
|
||||||
|
}
|
||||||
|
p, err := s.queries.CreateLmsPersona(ctx, dbgen.CreateLmsPersonaParams{
|
||||||
|
Name: in.Name,
|
||||||
|
Description: toPgText(in.Description),
|
||||||
|
AvatarUrl: toPgText(in.AvatarURL),
|
||||||
|
IsActive: active,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.LmsPersona{}, err
|
||||||
|
}
|
||||||
|
return lmsPersonaToDomain(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetLmsPersonaByID(ctx context.Context, id int64) (domain.LmsPersona, error) {
|
||||||
|
p, err := s.queries.GetLmsPersonaByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.LmsPersona{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.LmsPersona{}, err
|
||||||
|
}
|
||||||
|
return lmsPersonaToDomain(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateLmsPersona(ctx context.Context, id int64, in domain.UpdateLmsPersonaInput) (domain.LmsPersona, error) {
|
||||||
|
p, err := s.queries.UpdateLmsPersona(ctx, dbgen.UpdateLmsPersonaParams{
|
||||||
|
ID: id,
|
||||||
|
Name: optionalTextUpdate(in.Name),
|
||||||
|
Description: optionalTextUpdate(in.Description),
|
||||||
|
AvatarUrl: optionalTextUpdate(in.AvatarURL),
|
||||||
|
IsActive: optionalBoolUpdatePB(in.IsActive),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.LmsPersona{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.LmsPersona{}, err
|
||||||
|
}
|
||||||
|
return lmsPersonaToDomain(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteLmsPersona(ctx context.Context, id int64) error {
|
||||||
|
return s.queries.DeleteLmsPersona(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListLmsPersonas(ctx context.Context, activeOnly bool, limit, offset int32) ([]domain.LmsPersona, int64, error) {
|
||||||
|
rows, err := s.queries.ListLmsPersonas(ctx, dbgen.ListLmsPersonasParams{
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
FilterActive: activeOnly,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.LmsPersona{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.LmsPersona, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, lmsPersonaToDomain(dbgen.LmsPersona{
|
||||||
|
ID: r.ID,
|
||||||
|
Name: r.Name,
|
||||||
|
Description: r.Description,
|
||||||
|
AvatarUrl: r.AvatarUrl,
|
||||||
|
IsActive: r.IsActive,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
@ -15,13 +15,14 @@ var ErrModuleNotFound = errors.New("exam prep module not found")
|
||||||
var ErrLessonNotFound = errors.New("exam prep lesson not found")
|
var ErrLessonNotFound = errors.New("exam prep lesson not found")
|
||||||
var ErrPracticeNotFound = errors.New("exam prep practice not found")
|
var ErrPracticeNotFound = errors.New("exam prep practice not found")
|
||||||
|
|
||||||
// examPrepStore is implemented by *repository.Store (catalog courses, units, modules, lessons, practices).
|
// examPrepStore is implemented by *repository.Store (catalog courses, units, modules, lessons, practices, personas).
|
||||||
type examPrepStore interface {
|
type examPrepStore interface {
|
||||||
ports.ExamPrepCatalogCourseStore
|
ports.ExamPrepCatalogCourseStore
|
||||||
ports.ExamPrepUnitStore
|
ports.ExamPrepUnitStore
|
||||||
ports.ExamPrepModuleStore
|
ports.ExamPrepModuleStore
|
||||||
ports.ExamPrepLessonStore
|
ports.ExamPrepLessonStore
|
||||||
ports.ExamPrepPracticeStore
|
ports.ExamPrepPracticeStore
|
||||||
|
ports.LmsPersonaReader
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
|
@ -32,6 +33,17 @@ func NewService(store examPrepStore) *Service {
|
||||||
return &Service{store: store}
|
return &Service{store: store}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensurePersonaRef(ctx context.Context, id int64) error {
|
||||||
|
if id <= 0 {
|
||||||
|
return domain.ErrPersonaNotFound
|
||||||
|
}
|
||||||
|
_, err := s.store.GetLmsPersonaByID(ctx, id)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.ErrPersonaNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) CreateCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
|
func (s *Service) CreateCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
|
||||||
return s.store.CreateExamPrepCatalogCourse(ctx, input)
|
return s.store.CreateExamPrepCatalogCourse(ctx, input)
|
||||||
}
|
}
|
||||||
|
|
@ -355,6 +367,11 @@ func (s *Service) CreateExamPrepPractice(ctx context.Context, lessonID int64, in
|
||||||
if err := s.ensureLesson(ctx, lessonID); err != nil {
|
if err := s.ensureLesson(ctx, lessonID); err != nil {
|
||||||
return domain.ExamPrepPractice{}, err
|
return domain.ExamPrepPractice{}, err
|
||||||
}
|
}
|
||||||
|
if input.PersonaID != nil {
|
||||||
|
if err := s.ensurePersonaRef(ctx, *input.PersonaID); err != nil {
|
||||||
|
return domain.ExamPrepPractice{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return s.store.CreateExamPrepLessonPractice(ctx, lessonID, input)
|
return s.store.CreateExamPrepLessonPractice(ctx, lessonID, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,6 +407,11 @@ func (s *Service) TryGetExamPrepPracticeByQuestionSetID(ctx context.Context, que
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdateExamPrepPractice(ctx context.Context, id int64, input domain.UpdateExamPrepPracticeInput) (domain.ExamPrepPractice, error) {
|
func (s *Service) UpdateExamPrepPractice(ctx context.Context, id int64, input domain.UpdateExamPrepPracticeInput) (domain.ExamPrepPractice, error) {
|
||||||
|
if input.PersonaID != nil {
|
||||||
|
if err := s.ensurePersonaRef(ctx, *input.PersonaID); err != nil {
|
||||||
|
return domain.ExamPrepPractice{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
p, err := s.store.UpdateExamPrepLessonPractice(ctx, id, input)
|
p, err := s.store.UpdateExamPrepLessonPractice(ctx, id, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
|
|
||||||
91
internal/services/personas/service.go
Normal file
91
internal/services/personas/service.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
package personas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/ports"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrPersonaNotFound = domain.ErrPersonaNotFound
|
||||||
|
|
||||||
|
// ErrNameRequired indicates a missing trim-empty name on create.
|
||||||
|
var ErrNameRequired = errors.New("name is required")
|
||||||
|
|
||||||
|
// ErrNameEmptyUpdate indicates an update attempted to clear the persona name.
|
||||||
|
var ErrNameEmptyUpdate = errors.New("name cannot be empty")
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
store ports.LmsPersonaStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(store ports.LmsPersonaStore) *Service {
|
||||||
|
return &Service{store: store}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampPage(limit, offset int32) (int32, int32) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit > 200 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
return limit, offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(ctx context.Context, in domain.CreateLmsPersonaInput) (domain.LmsPersona, error) {
|
||||||
|
name := strings.TrimSpace(in.Name)
|
||||||
|
if name == "" {
|
||||||
|
return domain.LmsPersona{}, ErrNameRequired
|
||||||
|
}
|
||||||
|
in.Name = name
|
||||||
|
return s.store.CreateLmsPersona(ctx, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(ctx context.Context, id int64) (domain.LmsPersona, error) {
|
||||||
|
p, err := s.store.GetLmsPersonaByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.LmsPersona{}, ErrPersonaNotFound
|
||||||
|
}
|
||||||
|
return domain.LmsPersona{}, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(ctx context.Context, id int64, in domain.UpdateLmsPersonaInput) (domain.LmsPersona, error) {
|
||||||
|
if in.Name != nil {
|
||||||
|
t := strings.TrimSpace(*in.Name)
|
||||||
|
if t == "" {
|
||||||
|
return domain.LmsPersona{}, ErrNameEmptyUpdate
|
||||||
|
}
|
||||||
|
in.Name = &t
|
||||||
|
}
|
||||||
|
p, err := s.store.UpdateLmsPersona(ctx, id, in)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.LmsPersona{}, ErrPersonaNotFound
|
||||||
|
}
|
||||||
|
return domain.LmsPersona{}, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||||
|
if err := s.store.DeleteLmsPersona(ctx, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) List(ctx context.Context, activeOnly bool, limit, offset int32) ([]domain.LmsPersona, int64, error) {
|
||||||
|
limit, offset = clampPage(limit, offset)
|
||||||
|
return s.store.ListLmsPersonas(ctx, activeOnly, limit, offset)
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ type Service struct {
|
||||||
modules ports.ModuleStore
|
modules ports.ModuleStore
|
||||||
lessons ports.LessonStore
|
lessons ports.LessonStore
|
||||||
qs ports.QuestionSetByID
|
qs ports.QuestionSetByID
|
||||||
users ports.UserByID
|
personas ports.LmsPersonaReader
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
|
|
@ -33,7 +33,7 @@ func NewService(
|
||||||
modules ports.ModuleStore,
|
modules ports.ModuleStore,
|
||||||
lessons ports.LessonStore,
|
lessons ports.LessonStore,
|
||||||
qs ports.QuestionSetByID,
|
qs ports.QuestionSetByID,
|
||||||
users ports.UserByID,
|
personas ports.LmsPersonaReader,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
practices: practices,
|
practices: practices,
|
||||||
|
|
@ -41,7 +41,7 @@ func NewService(
|
||||||
modules: modules,
|
modules: modules,
|
||||||
lessons: lessons,
|
lessons: lessons,
|
||||||
qs: qs,
|
qs: qs,
|
||||||
users: users,
|
personas: personas,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,17 +56,19 @@ func (s *Service) validateQuestionSet(ctx context.Context, id int64) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) validatePersonaUser(ctx context.Context, id int64) error {
|
func (s *Service) validatePersonaCatalog(ctx context.Context, id int64) error {
|
||||||
_, err := s.users.GetUserByID(ctx, id)
|
if id <= 0 {
|
||||||
|
return domain.ErrPersonaNotFound
|
||||||
|
}
|
||||||
|
_, err := s.personas.GetLmsPersonaByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, domain.ErrUserNotFound) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return domain.ErrUserNotFound
|
return domain.ErrPersonaNotFound
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) resolveParent(ctx context.Context, in domain.CreatePracticeInput) (courseID, moduleID, lessonID *int64, err error) {
|
func (s *Service) resolveParent(ctx context.Context, in domain.CreatePracticeInput) (courseID, moduleID, lessonID *int64, err error) {
|
||||||
pid := in.ParentID
|
pid := in.ParentID
|
||||||
switch in.ParentKind {
|
switch in.ParentKind {
|
||||||
|
|
@ -104,7 +106,7 @@ func (s *Service) Create(ctx context.Context, in domain.CreatePracticeInput) (do
|
||||||
return domain.Practice{}, err
|
return domain.Practice{}, err
|
||||||
}
|
}
|
||||||
if in.PersonaID != nil {
|
if in.PersonaID != nil {
|
||||||
if err := s.validatePersonaUser(ctx, *in.PersonaID); err != nil {
|
if err := s.validatePersonaCatalog(ctx, *in.PersonaID); err != nil {
|
||||||
return domain.Practice{}, err
|
return domain.Practice{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +185,7 @@ func (s *Service) Update(ctx context.Context, id int64, input domain.UpdatePract
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if input.PersonaID != nil {
|
if input.PersonaID != nil {
|
||||||
if err := s.validatePersonaUser(ctx, *input.PersonaID); err != nil {
|
if err := s.validatePersonaCatalog(ctx, *input.PersonaID); err != nil {
|
||||||
return domain.Practice{}, err
|
return domain.Practice{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,13 @@ var AllPermissions = []domain.PermissionSeed{
|
||||||
{Key: "practices.update", Name: "Update Practice", Description: "Update a practice", GroupName: "Practices"},
|
{Key: "practices.update", Name: "Update Practice", Description: "Update a practice", GroupName: "Practices"},
|
||||||
{Key: "practices.delete", Name: "Delete Practice", Description: "Delete a practice", GroupName: "Practices"},
|
{Key: "practices.delete", Name: "Delete Practice", Description: "Delete a practice", GroupName: "Practices"},
|
||||||
|
|
||||||
|
// LMS personas (catalog for coach/character profiles linked on practices)
|
||||||
|
{Key: "personas.create", Name: "Create Persona", Description: "Create an LMS persona profile", GroupName: "Personas"},
|
||||||
|
{Key: "personas.list", Name: "List Personas", Description: "List LMS persona profiles", GroupName: "Personas"},
|
||||||
|
{Key: "personas.get", Name: "Get Persona", Description: "Get an LMS persona by ID", GroupName: "Personas"},
|
||||||
|
{Key: "personas.update", Name: "Update Persona", Description: "Update an LMS persona", GroupName: "Personas"},
|
||||||
|
{Key: "personas.delete", Name: "Delete Persona", Description: "Delete an LMS persona", GroupName: "Personas"},
|
||||||
|
|
||||||
// Course Management - Sub-courses
|
// Course Management - Sub-courses
|
||||||
{Key: "subcourses.create", Name: "Create Sub-course", Description: "Create a new sub-course", GroupName: "Sub-courses"},
|
{Key: "subcourses.create", Name: "Create Sub-course", Description: "Create a new sub-course", GroupName: "Sub-courses"},
|
||||||
{Key: "subcourses.get", Name: "Get Sub-course", Description: "Get a sub-course by ID", GroupName: "Sub-courses"},
|
{Key: "subcourses.get", Name: "Get Sub-course", Description: "Get a sub-course by ID", GroupName: "Sub-courses"},
|
||||||
|
|
@ -396,6 +403,9 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
// Practices
|
// Practices
|
||||||
"practices.create", "practices.get", "practices.list", "practices.update", "practices.delete",
|
"practices.create", "practices.get", "practices.list", "practices.update", "practices.delete",
|
||||||
|
|
||||||
|
// LMS personas catalog
|
||||||
|
"personas.create", "personas.list", "personas.get", "personas.update", "personas.delete",
|
||||||
|
|
||||||
// Questions (full access)
|
// Questions (full access)
|
||||||
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",
|
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",
|
||||||
"question_sets.create", "question_sets.list", "question_sets.list_by_owner", "question_sets.get", "question_sets.update", "question_sets.delete",
|
"question_sets.create", "question_sets.list", "question_sets.list_by_owner", "question_sets.get", "question_sets.update", "question_sets.delete",
|
||||||
|
|
@ -493,6 +503,8 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"exam_prep.practices.list_by_lesson", "exam_prep.practices.get",
|
"exam_prep.practices.list_by_lesson", "exam_prep.practices.get",
|
||||||
"lms.get_my_progress",
|
"lms.get_my_progress",
|
||||||
|
|
||||||
|
"personas.create", "personas.list", "personas.get", "personas.update", "personas.delete",
|
||||||
|
|
||||||
// Questions (full — instructors create content)
|
// Questions (full — instructors create content)
|
||||||
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",
|
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",
|
||||||
"question_sets.create", "question_sets.list", "question_sets.list_by_owner", "question_sets.get", "question_sets.update", "question_sets.delete",
|
"question_sets.create", "question_sets.list", "question_sets.list_by_owner", "question_sets.get", "question_sets.update", "question_sets.delete",
|
||||||
|
|
@ -551,6 +563,8 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"exam_prep.lessons.list_by_module", "exam_prep.lessons.get",
|
"exam_prep.lessons.list_by_module", "exam_prep.lessons.get",
|
||||||
"exam_prep.practices.list_by_lesson", "exam_prep.practices.get",
|
"exam_prep.practices.list_by_lesson", "exam_prep.practices.get",
|
||||||
|
|
||||||
|
"personas.list", "personas.get",
|
||||||
|
|
||||||
// Questions (read)
|
// Questions (read)
|
||||||
"questions.list", "questions.search", "questions.get",
|
"questions.list", "questions.search", "questions.get",
|
||||||
"question_sets.list", "question_sets.list_by_owner", "question_sets.get",
|
"question_sets.list", "question_sets.list_by_owner", "question_sets.get",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ import (
|
||||||
"Yimaru-Backend/internal/services/lmsprogress"
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
minioservice "Yimaru-Backend/internal/services/minio"
|
minioservice "Yimaru-Backend/internal/services/minio"
|
||||||
"Yimaru-Backend/internal/services/modules"
|
"Yimaru-Backend/internal/services/modules"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
|
"Yimaru-Backend/internal/services/personas"
|
||||||
"Yimaru-Backend/internal/services/practices"
|
"Yimaru-Backend/internal/services/practices"
|
||||||
"Yimaru-Backend/internal/services/programs"
|
"Yimaru-Backend/internal/services/programs"
|
||||||
"Yimaru-Backend/internal/services/questions"
|
"Yimaru-Backend/internal/services/questions"
|
||||||
|
|
@ -48,6 +49,7 @@ type App struct {
|
||||||
assessmentSvc *assessment.Service
|
assessmentSvc *assessment.Service
|
||||||
questionsSvc *questions.Service
|
questionsSvc *questions.Service
|
||||||
faqSvc *faqs.Service
|
faqSvc *faqs.Service
|
||||||
|
personaSvc *personas.Service
|
||||||
examPrepSvc *examprep.Service
|
examPrepSvc *examprep.Service
|
||||||
programSvc *programs.Service
|
programSvc *programs.Service
|
||||||
courseSvc *courses.Service
|
courseSvc *courses.Service
|
||||||
|
|
@ -87,6 +89,7 @@ func NewApp(
|
||||||
assessmentSvc *assessment.Service,
|
assessmentSvc *assessment.Service,
|
||||||
questionsSvc *questions.Service,
|
questionsSvc *questions.Service,
|
||||||
faqSvc *faqs.Service,
|
faqSvc *faqs.Service,
|
||||||
|
personaSvc *personas.Service,
|
||||||
examPrepSvc *examprep.Service,
|
examPrepSvc *examprep.Service,
|
||||||
programSvc *programs.Service,
|
programSvc *programs.Service,
|
||||||
courseSvc *courses.Service,
|
courseSvc *courses.Service,
|
||||||
|
|
@ -138,6 +141,7 @@ func NewApp(
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
faqSvc: faqSvc,
|
faqSvc: faqSvc,
|
||||||
|
personaSvc: personaSvc,
|
||||||
examPrepSvc: examPrepSvc,
|
examPrepSvc: examPrepSvc,
|
||||||
programSvc: programSvc,
|
programSvc: programSvc,
|
||||||
courseSvc: courseSvc,
|
courseSvc: courseSvc,
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ func (h *Handler) CreateExamPrepPractice(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, domain.ErrPersonaNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Persona not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to create practice",
|
Message: "Failed to create practice",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
|
|
@ -167,6 +173,12 @@ func (h *Handler) UpdateExamPrepPractice(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, domain.ErrPersonaNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Persona not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to update practice",
|
Message: "Failed to update practice",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ import (
|
||||||
"Yimaru-Backend/internal/services/lmsprogress"
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
minioservice "Yimaru-Backend/internal/services/minio"
|
minioservice "Yimaru-Backend/internal/services/minio"
|
||||||
"Yimaru-Backend/internal/services/modules"
|
"Yimaru-Backend/internal/services/modules"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
|
"Yimaru-Backend/internal/services/personas"
|
||||||
"Yimaru-Backend/internal/services/practices"
|
"Yimaru-Backend/internal/services/practices"
|
||||||
"Yimaru-Backend/internal/services/programs"
|
"Yimaru-Backend/internal/services/programs"
|
||||||
"Yimaru-Backend/internal/services/questions"
|
"Yimaru-Backend/internal/services/questions"
|
||||||
|
|
@ -47,6 +48,7 @@ type Handler struct {
|
||||||
assessmentSvc *assessment.Service
|
assessmentSvc *assessment.Service
|
||||||
questionsSvc *questions.Service
|
questionsSvc *questions.Service
|
||||||
faqSvc *faqs.Service
|
faqSvc *faqs.Service
|
||||||
|
personaSvc *personas.Service
|
||||||
examPrepSvc *examprep.Service
|
examPrepSvc *examprep.Service
|
||||||
programSvc *programs.Service
|
programSvc *programs.Service
|
||||||
courseSvc *courses.Service
|
courseSvc *courses.Service
|
||||||
|
|
@ -82,6 +84,7 @@ func New(
|
||||||
assessmentSvc *assessment.Service,
|
assessmentSvc *assessment.Service,
|
||||||
questionsSvc *questions.Service,
|
questionsSvc *questions.Service,
|
||||||
faqSvc *faqs.Service,
|
faqSvc *faqs.Service,
|
||||||
|
personaSvc *personas.Service,
|
||||||
examPrepSvc *examprep.Service,
|
examPrepSvc *examprep.Service,
|
||||||
programSvc *programs.Service,
|
programSvc *programs.Service,
|
||||||
courseSvc *courses.Service,
|
courseSvc *courses.Service,
|
||||||
|
|
@ -116,6 +119,7 @@ func New(
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
faqSvc: faqSvc,
|
faqSvc: faqSvc,
|
||||||
|
personaSvc: personaSvc,
|
||||||
examPrepSvc: examPrepSvc,
|
examPrepSvc: examPrepSvc,
|
||||||
programSvc: programSvc,
|
programSvc: programSvc,
|
||||||
courseSvc: courseSvc,
|
courseSvc: courseSvc,
|
||||||
|
|
|
||||||
166
internal/web_server/handlers/lms_persona_handler.go
Normal file
166
internal/web_server/handlers/lms_persona_handler.go
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"errors"
|
||||||
|
personasservice "Yimaru-Backend/internal/services/personas"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listPersonasData struct {
|
||||||
|
Personas []domain.LmsPersona `json:"personas"`
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePersona godoc
|
||||||
|
// @Summary Create LMS persona catalog entry
|
||||||
|
// @Tags personas
|
||||||
|
// @Accept json
|
||||||
|
// @Param body body domain.CreateLmsPersonaInput true "Persona"
|
||||||
|
// @Success 201 {object} domain.Response
|
||||||
|
// @Router /api/v1/personas [post]
|
||||||
|
func (h *Handler) CreatePersona(c *fiber.Ctx) error {
|
||||||
|
var req domain.CreateLmsPersonaInput
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Validation failed",
|
||||||
|
Error: firstValidationError(valErrs),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
p, err := h.personaSvc.Create(c.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, personasservice.ErrNameRequired) {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Validation failed", Error: err.Error()})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create persona", Error: err.Error()})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
|
Message: "Persona created successfully",
|
||||||
|
Data: p,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusCreated,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPersonas godoc
|
||||||
|
// @Summary List LMS personas (catalog for practice assignment)
|
||||||
|
// @Tags personas
|
||||||
|
// @Param active_only query bool false "When true (default), return only active personas" default(true)
|
||||||
|
// @Param limit query int false "Page size"
|
||||||
|
// @Param offset query int false "Offset"
|
||||||
|
// @Router /api/v1/personas [get]
|
||||||
|
func (h *Handler) ListPersonas(c *fiber.Ctx) error {
|
||||||
|
activeOnlyStr := strings.ToLower(strings.TrimSpace(c.Query("active_only", "true")))
|
||||||
|
activeOnly := activeOnlyStr != "false"
|
||||||
|
|
||||||
|
limit, err := strconv.Atoi(c.Query("limit", "20"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid limit", Error: err.Error()})
|
||||||
|
}
|
||||||
|
offset, err := strconv.Atoi(c.Query("offset", "0"))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid offset", Error: err.Error()})
|
||||||
|
}
|
||||||
|
items, total, err := h.personaSvc.List(c.Context(), activeOnly, int32(limit), int32(offset))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list personas", Error: err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Personas retrieved successfully",
|
||||||
|
Data: listPersonasData{
|
||||||
|
Personas: items,
|
||||||
|
TotalCount: total,
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
},
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPersona godoc
|
||||||
|
// @Summary Get LMS persona by ID
|
||||||
|
// @Tags personas
|
||||||
|
// @Param id path int true "Persona ID"
|
||||||
|
// @Router /api/v1/personas/{id} [get]
|
||||||
|
func (h *Handler) GetPersona(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
msg := ""
|
||||||
|
if err != nil {
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid persona id", Error: msg})
|
||||||
|
}
|
||||||
|
p, err := h.personaSvc.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, domain.ErrPersonaNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Persona not found", Error: err.Error()})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load persona", Error: err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{Message: "Persona retrieved successfully", Data: p, Success: true, StatusCode: fiber.StatusOK})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePersona godoc
|
||||||
|
// @Summary Update LMS persona
|
||||||
|
// @Tags personas
|
||||||
|
// @Param id path int true "Persona ID"
|
||||||
|
// @Param body body domain.UpdateLmsPersonaInput true "Fields to update"
|
||||||
|
// @Router /api/v1/personas/{id} [put]
|
||||||
|
func (h *Handler) UpdatePersona(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
msg := ""
|
||||||
|
if err != nil {
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid persona id", Error: msg})
|
||||||
|
}
|
||||||
|
var req domain.UpdateLmsPersonaInput
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()})
|
||||||
|
}
|
||||||
|
p, err := h.personaSvc.Update(c.Context(), id, req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, domain.ErrPersonaNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Persona not found", Error: err.Error()})
|
||||||
|
}
|
||||||
|
if errors.Is(err, personasservice.ErrNameEmptyUpdate) {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Validation failed", Error: err.Error()})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update persona", Error: err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{Message: "Persona updated successfully", Data: p, Success: true, StatusCode: fiber.StatusOK})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePersona godoc
|
||||||
|
// @Summary Delete LMS persona (practices referencing it will have persona_id cleared)
|
||||||
|
// @Tags personas
|
||||||
|
// @Param id path int true "Persona ID"
|
||||||
|
// @Router /api/v1/personas/{id} [delete]
|
||||||
|
func (h *Handler) DeletePersona(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
msg := ""
|
||||||
|
if err != nil {
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid persona id", Error: msg})
|
||||||
|
}
|
||||||
|
if err := h.personaSvc.Delete(c.Context(), id); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete persona", Error: err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{Message: "Persona deleted successfully", Success: true, StatusCode: fiber.StatusOK})
|
||||||
|
}
|
||||||
|
|
@ -43,8 +43,8 @@ func (h *Handler) CreatePractice(c *fiber.Ctx) error {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Lesson not found", Error: err.Error()})
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Lesson not found", Error: err.Error()})
|
||||||
case errors.Is(err, practices.ErrQuestionSetNotFound):
|
case errors.Is(err, practices.ErrQuestionSetNotFound):
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Question set not found", Error: err.Error()})
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Question set not found", Error: err.Error()})
|
||||||
case errors.Is(err, domain.ErrUserNotFound):
|
case errors.Is(err, domain.ErrPersonaNotFound):
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Persona user not found", Error: err.Error()})
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Persona not found", Error: err.Error()})
|
||||||
case errors.Is(err, practices.ErrInvalidPracticeParent):
|
case errors.Is(err, practices.ErrInvalidPracticeParent):
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid parent", Error: err.Error()})
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid parent", Error: err.Error()})
|
||||||
}
|
}
|
||||||
|
|
@ -205,8 +205,8 @@ func (h *Handler) UpdatePractice(c *fiber.Ctx) error {
|
||||||
if errors.Is(err, practices.ErrQuestionSetNotFound) {
|
if errors.Is(err, practices.ErrQuestionSetNotFound) {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Question set not found", Error: err.Error()})
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Question set not found", Error: err.Error()})
|
||||||
}
|
}
|
||||||
if errors.Is(err, domain.ErrUserNotFound) {
|
if errors.Is(err, domain.ErrPersonaNotFound) {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Persona user not found", Error: err.Error()})
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Persona not found", Error: err.Error()})
|
||||||
}
|
}
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice", Error: err.Error()})
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice", Error: err.Error()})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ func (a *App) initAppRoutes() {
|
||||||
a.assessmentSvc,
|
a.assessmentSvc,
|
||||||
a.questionsSvc,
|
a.questionsSvc,
|
||||||
a.faqSvc,
|
a.faqSvc,
|
||||||
|
a.personaSvc,
|
||||||
a.examPrepSvc,
|
a.examPrepSvc,
|
||||||
a.programSvc,
|
a.programSvc,
|
||||||
a.courseSvc,
|
a.courseSvc,
|
||||||
|
|
@ -151,6 +152,13 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Put("/practices/:id", a.authMiddleware, a.RequirePermission("practices.update"), h.UpdatePractice)
|
groupV1.Put("/practices/:id", a.authMiddleware, a.RequirePermission("practices.update"), h.UpdatePractice)
|
||||||
groupV1.Delete("/practices/:id", a.authMiddleware, a.RequirePermission("practices.delete"), h.DeletePractice)
|
groupV1.Delete("/practices/:id", a.authMiddleware, a.RequirePermission("practices.delete"), h.DeletePractice)
|
||||||
|
|
||||||
|
// LMS personas (catalog referenced by persona_id on practices)
|
||||||
|
groupV1.Get("/personas", a.authMiddleware, a.RequirePermission("personas.list"), h.ListPersonas)
|
||||||
|
groupV1.Post("/personas", a.authMiddleware, a.RequirePermission("personas.create"), h.CreatePersona)
|
||||||
|
groupV1.Get("/personas/:id", a.authMiddleware, a.RequirePermission("personas.get"), h.GetPersona)
|
||||||
|
groupV1.Put("/personas/:id", a.authMiddleware, a.RequirePermission("personas.update"), h.UpdatePersona)
|
||||||
|
groupV1.Delete("/personas/:id", a.authMiddleware, a.RequirePermission("personas.delete"), h.DeletePersona)
|
||||||
|
|
||||||
// File storage (MinIO)
|
// File storage (MinIO)
|
||||||
groupV1.Get("/files/url", a.authMiddleware, h.GetFileURL)
|
groupV1.Get("/files/url", a.authMiddleware, h.GetFileURL)
|
||||||
groupV1.Post("/files/refresh-url", a.authMiddleware, h.RefreshFileURL)
|
groupV1.Post("/files/refresh-url", a.authMiddleware, h.RefreshFileURL)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user