Compare commits
No commits in common. "c711df68b9ef0fc1c57768aa9e3d34d3089af4da" and "3d1b3ad9b884f3cc4fec0032d49d6714e63961d4" have entirely different histories.
c711df68b9
...
3d1b3ad9b8
23
cmd/main.go
23
cmd/main.go
|
|
@ -10,32 +10,31 @@ import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
customlogger "Yimaru-Backend/internal/logger"
|
customlogger "Yimaru-Backend/internal/logger"
|
||||||
"Yimaru-Backend/internal/logger/mongoLogger"
|
"Yimaru-Backend/internal/logger/mongoLogger"
|
||||||
minioclient "Yimaru-Backend/internal/pkgs/minio"
|
|
||||||
"Yimaru-Backend/internal/repository"
|
"Yimaru-Backend/internal/repository"
|
||||||
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
|
||||||
coursesservice "Yimaru-Backend/internal/services/courses"
|
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
|
||||||
"Yimaru-Backend/internal/services/faqs"
|
|
||||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||||
|
"Yimaru-Backend/internal/services/messenger"
|
||||||
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
|
coursesservice "Yimaru-Backend/internal/services/courses"
|
||||||
lessonsservice "Yimaru-Backend/internal/services/lessons"
|
lessonsservice "Yimaru-Backend/internal/services/lessons"
|
||||||
"Yimaru-Backend/internal/services/lmsprogress"
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
"Yimaru-Backend/internal/services/messenger"
|
|
||||||
minioservice "Yimaru-Backend/internal/services/minio"
|
|
||||||
moduleservice "Yimaru-Backend/internal/services/modules"
|
moduleservice "Yimaru-Backend/internal/services/modules"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
|
||||||
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"
|
||||||
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
"Yimaru-Backend/internal/services/examprep"
|
||||||
rbacservice "Yimaru-Backend/internal/services/rbac"
|
|
||||||
"Yimaru-Backend/internal/services/recommendation"
|
"Yimaru-Backend/internal/services/recommendation"
|
||||||
"Yimaru-Backend/internal/services/settings"
|
"Yimaru-Backend/internal/services/settings"
|
||||||
"Yimaru-Backend/internal/services/subscriptions"
|
"Yimaru-Backend/internal/services/subscriptions"
|
||||||
"Yimaru-Backend/internal/services/team"
|
"Yimaru-Backend/internal/services/team"
|
||||||
|
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||||
|
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
||||||
|
minioservice "Yimaru-Backend/internal/services/minio"
|
||||||
|
minioclient "Yimaru-Backend/internal/pkgs/minio"
|
||||||
|
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
||||||
|
rbacservice "Yimaru-Backend/internal/services/rbac"
|
||||||
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
|
@ -394,7 +393,6 @@ 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))
|
|
||||||
examPrepSvc := examprep.NewService(store)
|
examPrepSvc := examprep.NewService(store)
|
||||||
|
|
||||||
// LMS programs (top-level hierarchy)
|
// LMS programs (top-level hierarchy)
|
||||||
|
|
@ -455,7 +453,6 @@ func main() {
|
||||||
app := httpserver.NewApp(
|
app := httpserver.NewApp(
|
||||||
assessmentSvc,
|
assessmentSvc,
|
||||||
questionsSvc,
|
questionsSvc,
|
||||||
faqSvc,
|
|
||||||
examPrepSvc,
|
examPrepSvc,
|
||||||
programSvc,
|
programSvc,
|
||||||
courseSvc,
|
courseSvc,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
DROP INDEX IF EXISTS idx_faqs_display_order;
|
|
||||||
DROP INDEX IF EXISTS idx_faqs_category;
|
|
||||||
DROP INDEX IF EXISTS idx_faqs_status;
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS faqs;
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
CREATE TABLE IF NOT EXISTS faqs (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
question TEXT NOT NULL,
|
|
||||||
answer TEXT NOT NULL,
|
|
||||||
category VARCHAR(100),
|
|
||||||
display_order INT NOT NULL DEFAULT 0,
|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE')),
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_faqs_status ON faqs(status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_faqs_category ON faqs(category);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_faqs_display_order ON faqs(display_order);
|
|
||||||
|
|
@ -12,18 +12,9 @@ RETURNING
|
||||||
*;
|
*;
|
||||||
|
|
||||||
-- name: ExamPrepGetCatalogCourseByID :one
|
-- name: ExamPrepGetCatalogCourseByID :one
|
||||||
SELECT
|
SELECT *
|
||||||
c.*,
|
FROM exam_prep.catalog_courses
|
||||||
EXISTS (
|
WHERE id = $1;
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
|
|
||||||
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
|
|
||||||
INNER JOIN exam_prep.units u ON u.id = m.unit_id
|
|
||||||
WHERE u.catalog_course_id = c.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM exam_prep.catalog_courses c
|
|
||||||
WHERE c.id = $1;
|
|
||||||
|
|
||||||
-- name: ExamPrepListCatalogCourses :many
|
-- name: ExamPrepListCatalogCourses :many
|
||||||
WITH catalog_course_counts AS (
|
WITH catalog_course_counts AS (
|
||||||
|
|
@ -47,14 +38,6 @@ SELECT
|
||||||
COALESCE(cc.units_count, 0)::BIGINT AS units_count,
|
COALESCE(cc.units_count, 0)::BIGINT AS units_count,
|
||||||
COALESCE(cc.modules_count, 0)::BIGINT AS modules_count,
|
COALESCE(cc.modules_count, 0)::BIGINT AS modules_count,
|
||||||
COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_count,
|
COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_count,
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
|
|
||||||
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
|
|
||||||
INNER JOIN exam_prep.units u ON u.id = m.unit_id
|
|
||||||
WHERE u.catalog_course_id = c.id
|
|
||||||
) AS has_practice,
|
|
||||||
c.created_at,
|
c.created_at,
|
||||||
c.updated_at
|
c.updated_at
|
||||||
FROM exam_prep.catalog_courses c
|
FROM exam_prep.catalog_courses c
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,9 @@ RETURNING
|
||||||
*;
|
*;
|
||||||
|
|
||||||
-- name: ExamPrepGetUnitModuleLessonByID :one
|
-- name: ExamPrepGetUnitModuleLessonByID :one
|
||||||
SELECT
|
SELECT *
|
||||||
l.*,
|
FROM exam_prep.unit_module_lessons
|
||||||
EXISTS (
|
WHERE id = $1;
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
WHERE p.unit_module_lesson_id = l.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM exam_prep.unit_module_lessons l
|
|
||||||
WHERE l.id = $1;
|
|
||||||
|
|
||||||
-- name: ExamPrepListUnitModuleLessonIDsByUnitModule :many
|
-- name: ExamPrepListUnitModuleLessonIDsByUnitModule :many
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -45,11 +39,6 @@ SELECT
|
||||||
l.thumbnail,
|
l.thumbnail,
|
||||||
l.description,
|
l.description,
|
||||||
l.sort_order,
|
l.sort_order,
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
WHERE p.unit_module_lesson_id = l.id
|
|
||||||
) AS has_practice,
|
|
||||||
l.created_at,
|
l.created_at,
|
||||||
l.updated_at
|
l.updated_at
|
||||||
FROM exam_prep.unit_module_lessons l
|
FROM exam_prep.unit_module_lessons l
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,9 @@ RETURNING
|
||||||
*;
|
*;
|
||||||
|
|
||||||
-- name: ExamPrepGetUnitModuleByID :one
|
-- name: ExamPrepGetUnitModuleByID :one
|
||||||
SELECT
|
SELECT *
|
||||||
m.*,
|
FROM exam_prep.unit_modules
|
||||||
EXISTS (
|
WHERE id = $1;
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
|
|
||||||
WHERE l.unit_module_id = m.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM exam_prep.unit_modules m
|
|
||||||
WHERE m.id = $1;
|
|
||||||
|
|
||||||
-- name: ExamPrepListUnitModuleIDsByUnit :many
|
-- name: ExamPrepListUnitModuleIDsByUnit :many
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -58,7 +51,6 @@ SELECT
|
||||||
m.sort_order,
|
m.sort_order,
|
||||||
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
|
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
|
||||||
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
|
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
|
||||||
(COALESCE(mc.practices_count, 0)::BIGINT > 0) AS has_practice,
|
|
||||||
m.created_at,
|
m.created_at,
|
||||||
m.updated_at
|
m.updated_at
|
||||||
FROM exam_prep.unit_modules m
|
FROM exam_prep.unit_modules m
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,9 @@ RETURNING
|
||||||
*;
|
*;
|
||||||
|
|
||||||
-- name: ExamPrepGetUnitByID :one
|
-- name: ExamPrepGetUnitByID :one
|
||||||
SELECT
|
SELECT *
|
||||||
u.*,
|
FROM exam_prep.units
|
||||||
EXISTS (
|
WHERE id = $1;
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
|
|
||||||
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
|
|
||||||
WHERE m.unit_id = u.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM exam_prep.units u
|
|
||||||
WHERE u.id = $1;
|
|
||||||
|
|
||||||
-- name: ExamPrepListUnitIDsByCatalogCourse :many
|
-- name: ExamPrepListUnitIDsByCatalogCourse :many
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -60,7 +52,6 @@ SELECT
|
||||||
COALESCE(uc.modules_count, 0)::BIGINT AS modules_count,
|
COALESCE(uc.modules_count, 0)::BIGINT AS modules_count,
|
||||||
COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_count,
|
COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_count,
|
||||||
COALESCE(uc.practices_count, 0)::BIGINT AS practices_count,
|
COALESCE(uc.practices_count, 0)::BIGINT AS practices_count,
|
||||||
(COALESCE(uc.practices_count, 0)::BIGINT > 0) AS has_practice,
|
|
||||||
u.created_at,
|
u.created_at,
|
||||||
u.updated_at
|
u.updated_at
|
||||||
FROM exam_prep.units u
|
FROM exam_prep.units u
|
||||||
|
|
|
||||||
|
|
@ -15,18 +15,9 @@ RETURNING
|
||||||
*;
|
*;
|
||||||
|
|
||||||
-- name: GetCourseByID :one
|
-- name: GetCourseByID :one
|
||||||
SELECT
|
SELECT *
|
||||||
c.*,
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.course_id = c.id
|
|
||||||
AND p.module_id IS NULL
|
|
||||||
AND p.lesson_id IS NULL
|
|
||||||
) AS has_practice
|
|
||||||
FROM courses
|
FROM courses
|
||||||
c
|
WHERE id = $1;
|
||||||
WHERE c.id = $1;
|
|
||||||
|
|
||||||
-- name: ListCourseIDsByProgram :many
|
-- name: ListCourseIDsByProgram :many
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -74,14 +65,7 @@ SELECT
|
||||||
WHERE
|
WHERE
|
||||||
p.course_id = c.id
|
p.course_id = c.id
|
||||||
AND p.module_id IS NULL
|
AND p.module_id IS NULL
|
||||||
AND p.lesson_id IS NULL) AS practice_count,
|
AND p.lesson_id IS NULL) AS practice_count
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.course_id = c.id
|
|
||||||
AND p.module_id IS NULL
|
|
||||||
AND p.lesson_id IS NULL
|
|
||||||
) AS has_practice
|
|
||||||
FROM
|
FROM
|
||||||
courses c
|
courses c
|
||||||
WHERE
|
WHERE
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,9 @@ RETURNING
|
||||||
*;
|
*;
|
||||||
|
|
||||||
-- name: GetLessonByID :one
|
-- name: GetLessonByID :one
|
||||||
SELECT
|
SELECT *
|
||||||
l.*,
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.lesson_id = l.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM lessons
|
FROM lessons
|
||||||
l
|
WHERE id = $1;
|
||||||
WHERE l.id = $1;
|
|
||||||
|
|
||||||
-- name: ListLessonsByModuleID :many
|
-- name: ListLessonsByModuleID :many
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -38,12 +31,7 @@ SELECT
|
||||||
l.description,
|
l.description,
|
||||||
l.sort_order,
|
l.sort_order,
|
||||||
l.created_at,
|
l.created_at,
|
||||||
l.updated_at,
|
l.updated_at
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.lesson_id = l.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM
|
FROM
|
||||||
lessons l
|
lessons l
|
||||||
WHERE
|
WHERE
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,9 @@ RETURNING
|
||||||
*;
|
*;
|
||||||
|
|
||||||
-- name: GetModuleByID :one
|
-- name: GetModuleByID :one
|
||||||
SELECT
|
SELECT *
|
||||||
m.*,
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.module_id = m.id
|
|
||||||
AND p.lesson_id IS NULL
|
|
||||||
) AS has_practice
|
|
||||||
FROM modules
|
FROM modules
|
||||||
m
|
WHERE id = $1;
|
||||||
WHERE m.id = $1;
|
|
||||||
|
|
||||||
-- name: ListModuleIDsByCourse :many
|
-- name: ListModuleIDsByCourse :many
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -49,13 +41,7 @@ SELECT
|
||||||
m.icon,
|
m.icon,
|
||||||
m.sort_order,
|
m.sort_order,
|
||||||
m.created_at,
|
m.created_at,
|
||||||
m.updated_at,
|
m.updated_at
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.module_id = m.id
|
|
||||||
AND p.lesson_id IS NULL
|
|
||||||
) AS has_practice
|
|
||||||
FROM
|
FROM
|
||||||
modules m
|
modules m
|
||||||
WHERE
|
WHERE
|
||||||
|
|
|
||||||
|
|
@ -1,412 +0,0 @@
|
||||||
# Dynamic Question Type Builder — Admin Panel Integration Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This guide explains how to integrate the backend **Dynamic Question Type Builder** into your admin panel app so admins can:
|
|
||||||
|
|
||||||
1. Create reusable dynamic question type definitions
|
|
||||||
2. Configure schema-driven stimulus/response fields
|
|
||||||
3. Create `DYNAMIC` questions using those definitions
|
|
||||||
4. Add dynamic questions into question sets/practices
|
|
||||||
|
|
||||||
The backend already supports:
|
|
||||||
- Component catalog discovery
|
|
||||||
- Definition validation + CRUD
|
|
||||||
- Dynamic question create/update/list/get with payload validation
|
|
||||||
- Persistence of `question_type_definition_id` and `dynamic_payload`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature Architecture (Admin Perspective)
|
|
||||||
|
|
||||||
Treat the integration as two linked modules:
|
|
||||||
|
|
||||||
1. **Definition Builder Module**
|
|
||||||
- Creates reusable templates (question type definitions)
|
|
||||||
- Managed by admins before creating actual questions
|
|
||||||
|
|
||||||
2. **Dynamic Question Authoring Module**
|
|
||||||
- Creates question instances using a selected definition
|
|
||||||
- Stores content in `dynamic_payload`
|
|
||||||
|
|
||||||
Recommended admin menu structure:
|
|
||||||
- `Questions > Type Builder > Definitions`
|
|
||||||
- `Questions > Create Question` (includes `DYNAMIC` path)
|
|
||||||
- `Question Sets > Manage Set Items`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backend Endpoints Used
|
|
||||||
|
|
||||||
All endpoints are under `/api/v1` and require bearer auth.
|
|
||||||
|
|
||||||
### Builder + Definition Endpoints
|
|
||||||
|
|
||||||
| Method | Endpoint | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `GET` | `/questions/component-catalog` | Fetch valid component kind codes |
|
|
||||||
| `POST` | `/questions/validate-question-type-definition` | Validate component-kind selection |
|
|
||||||
| `POST` | `/questions/type-definitions` | Create reusable definition |
|
|
||||||
| `GET` | `/questions/type-definitions` | List definitions |
|
|
||||||
| `GET` | `/questions/type-definitions/:id` | Get one definition |
|
|
||||||
| `PUT` | `/questions/type-definitions/:id` | Update definition |
|
|
||||||
| `DELETE` | `/questions/type-definitions/:id` | Delete non-system definition |
|
|
||||||
|
|
||||||
### Dynamic Question Endpoints
|
|
||||||
|
|
||||||
| Method | Endpoint | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `POST` | `/questions` | Create a dynamic question |
|
|
||||||
| `PUT` | `/questions/:id` | Update dynamic question |
|
|
||||||
| `GET` | `/questions/:id` | Fetch question details |
|
|
||||||
| `GET` | `/questions?question_type=DYNAMIC` | List dynamic questions |
|
|
||||||
|
|
||||||
### Question Set Linking Endpoints
|
|
||||||
|
|
||||||
| Method | Endpoint | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `POST` | `/question-sets` | Create set (if needed) |
|
|
||||||
| `POST` | `/question-sets/:setId/questions` | Link question to set |
|
|
||||||
| `GET` | `/question-sets/:setId/questions` | View set questions |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Required RBAC Permissions
|
|
||||||
|
|
||||||
Ensure the admin role includes at least:
|
|
||||||
|
|
||||||
- `questions.create`
|
|
||||||
- `questions.list`
|
|
||||||
- `questions.get`
|
|
||||||
- `questions.update`
|
|
||||||
- `questions.delete`
|
|
||||||
- `question_sets.create`
|
|
||||||
- `question_set_items.add`
|
|
||||||
- `question_set_items.list`
|
|
||||||
- `question_set_items.remove`
|
|
||||||
- `question_set_items.update_order`
|
|
||||||
|
|
||||||
If your UI supports definition delete/update/list, include corresponding question permissions already used by those routes (`questions.update`, `questions.delete`, etc.).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Contracts
|
|
||||||
|
|
||||||
### 1) Definition Create Payload
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"key": "dynamic_visual_mcq_001",
|
|
||||||
"display_name": "Dynamic Visual MCQ",
|
|
||||||
"description": "Reusable dynamic MCQ-style type",
|
|
||||||
"stimulus_component_kinds": ["QUESTION_TEXT", "IMAGE", "TABLE"],
|
|
||||||
"response_component_kinds": ["OPTION", "ANSWER_TIMER"],
|
|
||||||
"stimulus_schema": [
|
|
||||||
{
|
|
||||||
"id": "prompt",
|
|
||||||
"kind": "QUESTION_TEXT",
|
|
||||||
"label": "Prompt",
|
|
||||||
"required": true,
|
|
||||||
"config": { "max_length": 1000 }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"response_schema": [
|
|
||||||
{
|
|
||||||
"id": "choices",
|
|
||||||
"kind": "OPTION",
|
|
||||||
"label": "Answer Choices",
|
|
||||||
"required": true,
|
|
||||||
"config": { "min_options": 2, "max_options": 6, "allow_multiple": false }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "timer",
|
|
||||||
"kind": "ANSWER_TIMER",
|
|
||||||
"label": "Timer",
|
|
||||||
"required": false,
|
|
||||||
"config": { "min_seconds": 5, "max_seconds": 180 }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "ACTIVE"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2) Dynamic Question Create Payload
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"question_text": "Choose the grammatically correct sentence.",
|
|
||||||
"question_type": "DYNAMIC",
|
|
||||||
"question_type_definition_id": 123,
|
|
||||||
"difficulty_level": "MEDIUM",
|
|
||||||
"points": 2,
|
|
||||||
"status": "DRAFT",
|
|
||||||
"dynamic_payload": {
|
|
||||||
"stimulus": [
|
|
||||||
{
|
|
||||||
"id": "prompt",
|
|
||||||
"kind": "QUESTION_TEXT",
|
|
||||||
"value": "Pick the best sentence."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "illustration",
|
|
||||||
"kind": "IMAGE",
|
|
||||||
"value": "https://cdn.example.com/image.png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"response": [
|
|
||||||
{
|
|
||||||
"id": "choices",
|
|
||||||
"kind": "OPTION",
|
|
||||||
"value": {
|
|
||||||
"options": [
|
|
||||||
{ "id": "a", "text": "He go yesterday", "is_correct": false },
|
|
||||||
{ "id": "b", "text": "He went yesterday", "is_correct": true }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "timer",
|
|
||||||
"kind": "ANSWER_TIMER",
|
|
||||||
"value": { "seconds": 30 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## End-to-End Admin Workflow
|
|
||||||
|
|
||||||
## Step 1: Load Component Catalog
|
|
||||||
|
|
||||||
Call:
|
|
||||||
- `GET /questions/component-catalog`
|
|
||||||
|
|
||||||
Use response values to populate:
|
|
||||||
- Stimulus component type selector
|
|
||||||
- Response component type selector
|
|
||||||
|
|
||||||
Do not hardcode component kinds in UI.
|
|
||||||
|
|
||||||
## Step 2: Build "Create Definition" Form
|
|
||||||
|
|
||||||
Form fields:
|
|
||||||
- `key` (slug-like identifier, unique)
|
|
||||||
- `display_name`
|
|
||||||
- `description` (optional)
|
|
||||||
- `stimulus_component_kinds` (multi-select)
|
|
||||||
- `response_component_kinds` (multi-select)
|
|
||||||
- `stimulus_schema` (repeater builder)
|
|
||||||
- `response_schema` (repeater builder)
|
|
||||||
- `status` (`ACTIVE` or `INACTIVE`)
|
|
||||||
|
|
||||||
Schema item editor fields:
|
|
||||||
- `id` (string, unique per side)
|
|
||||||
- `kind` (from catalog)
|
|
||||||
- `label` (optional)
|
|
||||||
- `required` (boolean)
|
|
||||||
- `config` (JSON object, optional)
|
|
||||||
|
|
||||||
## Step 3: Validate Component Kind Selection (Optional Pre-check)
|
|
||||||
|
|
||||||
Call:
|
|
||||||
- `POST /questions/validate-question-type-definition`
|
|
||||||
|
|
||||||
Use this before submit for immediate feedback.
|
|
||||||
Important: this endpoint validates component selection, while create endpoint enforces full persistence checks.
|
|
||||||
|
|
||||||
## Step 4: Create Definition
|
|
||||||
|
|
||||||
Call:
|
|
||||||
- `POST /questions/type-definitions`
|
|
||||||
|
|
||||||
On success:
|
|
||||||
- store returned definition `id`
|
|
||||||
- navigate to "Definition Details" view
|
|
||||||
- show copyable key + id
|
|
||||||
|
|
||||||
## Step 5: Build Definitions List Screen
|
|
||||||
|
|
||||||
Call:
|
|
||||||
- `GET /questions/type-definitions?include_system=true&status=ACTIVE`
|
|
||||||
|
|
||||||
Recommended columns:
|
|
||||||
- `id`
|
|
||||||
- `key`
|
|
||||||
- `display_name`
|
|
||||||
- `is_system`
|
|
||||||
- `status`
|
|
||||||
- stimulus/response kind counts
|
|
||||||
- created date
|
|
||||||
|
|
||||||
Actions:
|
|
||||||
- View details
|
|
||||||
- Edit
|
|
||||||
- Delete (disable for `is_system=true`)
|
|
||||||
|
|
||||||
## Step 6: Create Dynamic Question Using Definition
|
|
||||||
|
|
||||||
In question create UI:
|
|
||||||
|
|
||||||
1. Admin chooses question mode: `DYNAMIC`
|
|
||||||
2. Admin selects definition from searchable list (`GET /questions/type-definitions`)
|
|
||||||
3. UI generates form sections from selected definition schema
|
|
||||||
4. UI builds `dynamic_payload.stimulus[]` and `dynamic_payload.response[]`
|
|
||||||
5. Submit `POST /questions`
|
|
||||||
|
|
||||||
Rules to enforce in UI:
|
|
||||||
- `question_type` must be `DYNAMIC`
|
|
||||||
- `question_type_definition_id` is required
|
|
||||||
- `dynamic_payload` is required
|
|
||||||
- include all required schema item IDs
|
|
||||||
- each payload item `kind` must match allowed kinds for selected definition
|
|
||||||
|
|
||||||
## Step 7: Edit Dynamic Question
|
|
||||||
|
|
||||||
Call:
|
|
||||||
- `PUT /questions/:id`
|
|
||||||
|
|
||||||
When loading edit page:
|
|
||||||
- `GET /questions/:id`
|
|
||||||
- if `question_type = DYNAMIC`, hydrate schema-based editors from `dynamic_payload`
|
|
||||||
|
|
||||||
## Step 8: Add Dynamic Question to Set
|
|
||||||
|
|
||||||
Call:
|
|
||||||
- `POST /question-sets/:setId/questions`
|
|
||||||
|
|
||||||
Body:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"question_id": 456,
|
|
||||||
"display_order": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Frontend UI/State Design
|
|
||||||
|
|
||||||
Recommended frontend state model:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type DynamicElementDefinition = {
|
|
||||||
id: string;
|
|
||||||
kind: string;
|
|
||||||
label?: string;
|
|
||||||
required: boolean;
|
|
||||||
config?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DynamicElementInstance = {
|
|
||||||
id: string;
|
|
||||||
kind: string;
|
|
||||||
value?: unknown;
|
|
||||||
meta?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DynamicQuestionPayload = {
|
|
||||||
stimulus: DynamicElementInstance[];
|
|
||||||
response: DynamicElementInstance[];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Suggested screen components:
|
|
||||||
- `DefinitionForm`
|
|
||||||
- `SchemaBuilderTable`
|
|
||||||
- `DynamicQuestionComposer`
|
|
||||||
- `DynamicPayloadPreviewJson`
|
|
||||||
- `DefinitionPickerModal`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Validation Rules You Should Mirror Client-Side
|
|
||||||
|
|
||||||
To reduce failed submits, mirror these checks:
|
|
||||||
|
|
||||||
1. At least one stimulus component kind
|
|
||||||
2. At least one response component kind
|
|
||||||
3. No duplicate component kinds
|
|
||||||
4. `ANSWER_TIMER` cannot be the only response kind
|
|
||||||
5. Schema item IDs are required and unique per side
|
|
||||||
6. Schema kinds must be valid catalog kinds
|
|
||||||
7. Dynamic question must include definition ID + payload together
|
|
||||||
8. For dynamic questions, `question_type` must be `DYNAMIC`
|
|
||||||
9. Payload must include required schema IDs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling Strategy
|
|
||||||
|
|
||||||
Backend commonly returns:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Invalid dynamic_payload",
|
|
||||||
"error": "dynamic_payload.response: required element id \"choices\" is missing"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
UI recommendations:
|
|
||||||
- Show `message` as toast title
|
|
||||||
- Show `error` inline near relevant section
|
|
||||||
- Keep submitted form state (do not reset on 400)
|
|
||||||
- If schema mismatch occurs, provide "Rebuild from Definition" action
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Practical Example: Dynamic MCQ Type
|
|
||||||
|
|
||||||
Use this pattern to represent multiple-choice dynamically:
|
|
||||||
|
|
||||||
- `response_component_kinds` includes `OPTION`
|
|
||||||
- `response_schema` has required element with `kind: "OPTION"` and id `choices`
|
|
||||||
- `dynamic_payload.response` contains an element with same id (`choices`) and options array in `value`
|
|
||||||
|
|
||||||
This creates an MCQ-like authoring experience through the dynamic builder.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Important Product Note
|
|
||||||
|
|
||||||
This backend fully supports authoring/storage/serving dynamic questions.
|
|
||||||
If your admin panel expects automatic learner scoring for all dynamic payload shapes, verify assessment runtime behavior separately before release.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## QA Checklist (Admin Panel)
|
|
||||||
|
|
||||||
- [ ] Catalog loads and renders kind options correctly
|
|
||||||
- [ ] Definition create succeeds with valid schema
|
|
||||||
- [ ] Invalid schema shows clear inline errors
|
|
||||||
- [ ] System definitions cannot be deleted in UI
|
|
||||||
- [ ] Dynamic question cannot submit without definition ID
|
|
||||||
- [ ] Dynamic question cannot submit without payload
|
|
||||||
- [ ] Required schema fields enforce data entry
|
|
||||||
- [ ] Created question appears in `question_type=DYNAMIC` list
|
|
||||||
- [ ] Dynamic question can be edited and re-saved
|
|
||||||
- [ ] Dynamic question can be linked to question set
|
|
||||||
- [ ] Permission errors (`403`) are surfaced with actionable text
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Suggested Delivery Plan
|
|
||||||
|
|
||||||
1. Implement definition list/create/edit screens
|
|
||||||
2. Implement dynamic question composer tied to selected definition
|
|
||||||
3. Add question set linking support
|
|
||||||
4. Add role/permission guard handling in UI
|
|
||||||
5. Add integration tests for create/edit flows
|
|
||||||
6. Run UAT with at least one MCQ-like dynamic definition and one non-MCQ definition
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reference Artifact
|
|
||||||
|
|
||||||
There is a Postman collection in this repository that covers runtime flow:
|
|
||||||
|
|
||||||
- `postman/Dynamic-Question-Type-Builder.postman_collection.json`
|
|
||||||
|
|
||||||
Use it as the source of truth for request examples while building admin API services.
|
|
||||||
475
docs/docs.go
475
docs/docs.go
|
|
@ -254,257 +254,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/admin/faqs": {
|
|
||||||
"get": {
|
|
||||||
"description": "Returns FAQs for admin management with status/category filtering",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "List FAQs (admin)",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "ACTIVE or INACTIVE",
|
|
||||||
"name": "status",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Filter by category",
|
|
||||||
"name": "category",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Limit (default 20)",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Offset (default 0)",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"description": "Creates a new FAQ item",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "Create FAQ",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Create FAQ payload",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handlers.createFAQReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Created",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/faqs/{id}": {
|
|
||||||
"get": {
|
|
||||||
"description": "Returns one FAQ regardless of status",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "Get FAQ by ID (admin)",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "FAQ ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"description": "Updates an existing FAQ item",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "Update FAQ",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "FAQ ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Update FAQ payload",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handlers.updateFAQReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"description": "Deletes an FAQ item",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "Delete FAQ",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "FAQ ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/users/deletion-requests": {
|
"/api/v1/admin/users/deletion-requests": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns account deletion requests for admin panel tracking with filtering and pagination",
|
"description": "Returns account deletion requests for admin panel tracking with filtering and pagination",
|
||||||
|
|
@ -1859,99 +1608,6 @@ const docTemplate = `{
|
||||||
"responses": {}
|
"responses": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/faqs": {
|
|
||||||
"get": {
|
|
||||||
"description": "Returns active FAQs for public/help center usage",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "List published FAQs",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Filter by category",
|
|
||||||
"name": "category",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Limit (default 50)",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Offset (default 0)",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/faqs/{id}": {
|
|
||||||
"get": {
|
|
||||||
"description": "Returns one active FAQ item",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "Get published FAQ by ID",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "FAQ ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/files/audio": {
|
"/api/v1/files/audio": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
|
@ -9749,60 +9405,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.DynamicElementDefinition": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"config": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"kind": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"label": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"required": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.DynamicElementInstance": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"kind": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true
|
|
||||||
},
|
|
||||||
"value": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.DynamicQuestionPayload": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"response": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementInstance"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stimulus": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementInstance"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.EmploymentType": {
|
"domain.EmploymentType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -10036,9 +9638,6 @@ const docTemplate = `{
|
||||||
"difficultyLevel": {
|
"difficultyLevel": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"dynamicPayload": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicQuestionPayload"
|
|
||||||
},
|
|
||||||
"explanation": {
|
"explanation": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11252,30 +10851,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers.createFAQReq": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"answer",
|
|
||||||
"question"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"answer": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"category": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"display_order": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"question": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.createIssueReq": {
|
"handlers.createIssueReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11352,9 +10927,6 @@ const docTemplate = `{
|
||||||
"difficulty_level": {
|
"difficulty_level": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"dynamic_payload": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicQuestionPayload"
|
|
||||||
},
|
|
||||||
"explanation": {
|
"explanation": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11474,12 +11046,6 @@ const docTemplate = `{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"response_schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementDefinition"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": {
|
"status": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11488,12 +11054,6 @@ const docTemplate = `{
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"stimulus_schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementDefinition"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -11786,26 +11346,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers.updateFAQReq": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"answer": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"category": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"display_order": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"question": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.updateIssueStatusReq": {
|
"handlers.updateIssueStatusReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11869,9 +11409,6 @@ const docTemplate = `{
|
||||||
"difficulty_level": {
|
"difficulty_level": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"dynamic_payload": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicQuestionPayload"
|
|
||||||
},
|
|
||||||
"explanation": {
|
"explanation": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11963,12 +11500,6 @@ const docTemplate = `{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"response_schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementDefinition"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": {
|
"status": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11977,12 +11508,6 @@ const docTemplate = `{
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"stimulus_schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementDefinition"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -246,257 +246,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/admin/faqs": {
|
|
||||||
"get": {
|
|
||||||
"description": "Returns FAQs for admin management with status/category filtering",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "List FAQs (admin)",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "ACTIVE or INACTIVE",
|
|
||||||
"name": "status",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Filter by category",
|
|
||||||
"name": "category",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Limit (default 20)",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Offset (default 0)",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"description": "Creates a new FAQ item",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "Create FAQ",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Create FAQ payload",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handlers.createFAQReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Created",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/faqs/{id}": {
|
|
||||||
"get": {
|
|
||||||
"description": "Returns one FAQ regardless of status",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "Get FAQ by ID (admin)",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "FAQ ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"description": "Updates an existing FAQ item",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "Update FAQ",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "FAQ ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Update FAQ payload",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handlers.updateFAQReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"description": "Deletes an FAQ item",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "Delete FAQ",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "FAQ ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/admin/users/deletion-requests": {
|
"/api/v1/admin/users/deletion-requests": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns account deletion requests for admin panel tracking with filtering and pagination",
|
"description": "Returns account deletion requests for admin panel tracking with filtering and pagination",
|
||||||
|
|
@ -1851,99 +1600,6 @@
|
||||||
"responses": {}
|
"responses": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/faqs": {
|
|
||||||
"get": {
|
|
||||||
"description": "Returns active FAQs for public/help center usage",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "List published FAQs",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Filter by category",
|
|
||||||
"name": "category",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Limit (default 50)",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Offset (default 0)",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/faqs/{id}": {
|
|
||||||
"get": {
|
|
||||||
"description": "Returns one active FAQ item",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"summary": "Get published FAQ by ID",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "FAQ ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Not Found",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/files/audio": {
|
"/api/v1/files/audio": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
|
@ -9741,60 +9397,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.DynamicElementDefinition": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"config": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"kind": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"label": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"required": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.DynamicElementInstance": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"kind": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": true
|
|
||||||
},
|
|
||||||
"value": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.DynamicQuestionPayload": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"response": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementInstance"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stimulus": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementInstance"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.EmploymentType": {
|
"domain.EmploymentType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -10028,9 +9630,6 @@
|
||||||
"difficultyLevel": {
|
"difficultyLevel": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"dynamicPayload": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicQuestionPayload"
|
|
||||||
},
|
|
||||||
"explanation": {
|
"explanation": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11244,30 +10843,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers.createFAQReq": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"answer",
|
|
||||||
"question"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"answer": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"category": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"display_order": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"question": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.createIssueReq": {
|
"handlers.createIssueReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11344,9 +10919,6 @@
|
||||||
"difficulty_level": {
|
"difficulty_level": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"dynamic_payload": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicQuestionPayload"
|
|
||||||
},
|
|
||||||
"explanation": {
|
"explanation": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11466,12 +11038,6 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"response_schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementDefinition"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": {
|
"status": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11480,12 +11046,6 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"stimulus_schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementDefinition"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -11778,26 +11338,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers.updateFAQReq": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"answer": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"category": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"display_order": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"question": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.updateIssueStatusReq": {
|
"handlers.updateIssueStatusReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11861,9 +11401,6 @@
|
||||||
"difficulty_level": {
|
"difficulty_level": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"dynamic_payload": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicQuestionPayload"
|
|
||||||
},
|
|
||||||
"explanation": {
|
"explanation": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11955,12 +11492,6 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"response_schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementDefinition"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": {
|
"status": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11969,12 +11500,6 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"stimulus_schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.DynamicElementDefinition"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -212,42 +212,6 @@ definitions:
|
||||||
- password
|
- password
|
||||||
- team_role
|
- team_role
|
||||||
type: object
|
type: object
|
||||||
domain.DynamicElementDefinition:
|
|
||||||
properties:
|
|
||||||
config:
|
|
||||||
additionalProperties: true
|
|
||||||
type: object
|
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
kind:
|
|
||||||
type: string
|
|
||||||
label:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
domain.DynamicElementInstance:
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
kind:
|
|
||||||
type: string
|
|
||||||
meta:
|
|
||||||
additionalProperties: true
|
|
||||||
type: object
|
|
||||||
value: {}
|
|
||||||
type: object
|
|
||||||
domain.DynamicQuestionPayload:
|
|
||||||
properties:
|
|
||||||
response:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/domain.DynamicElementInstance'
|
|
||||||
type: array
|
|
||||||
stimulus:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/domain.DynamicElementInstance'
|
|
||||||
type: array
|
|
||||||
type: object
|
|
||||||
domain.EmploymentType:
|
domain.EmploymentType:
|
||||||
enum:
|
enum:
|
||||||
- full_time
|
- full_time
|
||||||
|
|
@ -407,8 +371,6 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
difficultyLevel:
|
difficultyLevel:
|
||||||
type: string
|
type: string
|
||||||
dynamicPayload:
|
|
||||||
$ref: '#/definitions/domain.DynamicQuestionPayload'
|
|
||||||
explanation:
|
explanation:
|
||||||
type: string
|
type: string
|
||||||
id:
|
id:
|
||||||
|
|
@ -1226,22 +1188,6 @@ definitions:
|
||||||
- current_password
|
- current_password
|
||||||
- new_password
|
- new_password
|
||||||
type: object
|
type: object
|
||||||
handlers.createFAQReq:
|
|
||||||
properties:
|
|
||||||
answer:
|
|
||||||
type: string
|
|
||||||
category:
|
|
||||||
type: string
|
|
||||||
display_order:
|
|
||||||
type: integer
|
|
||||||
question:
|
|
||||||
type: string
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- answer
|
|
||||||
- question
|
|
||||||
type: object
|
|
||||||
handlers.createIssueReq:
|
handlers.createIssueReq:
|
||||||
properties:
|
properties:
|
||||||
description:
|
description:
|
||||||
|
|
@ -1294,8 +1240,6 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
difficulty_level:
|
difficulty_level:
|
||||||
type: string
|
type: string
|
||||||
dynamic_payload:
|
|
||||||
$ref: '#/definitions/domain.DynamicQuestionPayload'
|
|
||||||
explanation:
|
explanation:
|
||||||
type: string
|
type: string
|
||||||
image_url:
|
image_url:
|
||||||
|
|
@ -1376,20 +1320,12 @@ definitions:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
response_schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/domain.DynamicElementDefinition'
|
|
||||||
type: array
|
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
stimulus_component_kinds:
|
stimulus_component_kinds:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
stimulus_schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/domain.DynamicElementDefinition'
|
|
||||||
type: array
|
|
||||||
required:
|
required:
|
||||||
- display_name
|
- display_name
|
||||||
- key
|
- key
|
||||||
|
|
@ -1589,19 +1525,6 @@ definitions:
|
||||||
example: false
|
example: false
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
handlers.updateFAQReq:
|
|
||||||
properties:
|
|
||||||
answer:
|
|
||||||
type: string
|
|
||||||
category:
|
|
||||||
type: string
|
|
||||||
display_order:
|
|
||||||
type: integer
|
|
||||||
question:
|
|
||||||
type: string
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
handlers.updateIssueStatusReq:
|
handlers.updateIssueStatusReq:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
|
|
@ -1644,8 +1567,6 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
difficulty_level:
|
difficulty_level:
|
||||||
type: string
|
type: string
|
||||||
dynamic_payload:
|
|
||||||
$ref: '#/definitions/domain.DynamicQuestionPayload'
|
|
||||||
explanation:
|
explanation:
|
||||||
type: string
|
type: string
|
||||||
image_url:
|
image_url:
|
||||||
|
|
@ -1706,20 +1627,12 @@ definitions:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
response_schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/domain.DynamicElementDefinition'
|
|
||||||
type: array
|
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
stimulus_component_kinds:
|
stimulus_component_kinds:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
stimulus_schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/domain.DynamicElementDefinition'
|
|
||||||
type: array
|
|
||||||
type: object
|
type: object
|
||||||
handlers.validateQuestionTypeDefinitionReq:
|
handlers.validateQuestionTypeDefinitionReq:
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -2371,172 +2284,6 @@ paths:
|
||||||
summary: Update Admin
|
summary: Update Admin
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- admin
|
||||||
/api/v1/admin/faqs:
|
|
||||||
get:
|
|
||||||
description: Returns FAQs for admin management with status/category filtering
|
|
||||||
parameters:
|
|
||||||
- description: ACTIVE or INACTIVE
|
|
||||||
in: query
|
|
||||||
name: status
|
|
||||||
type: string
|
|
||||||
- description: Filter by category
|
|
||||||
in: query
|
|
||||||
name: category
|
|
||||||
type: string
|
|
||||||
- description: Limit (default 20)
|
|
||||||
in: query
|
|
||||||
name: limit
|
|
||||||
type: integer
|
|
||||||
- description: Offset (default 0)
|
|
||||||
in: query
|
|
||||||
name: offset
|
|
||||||
type: integer
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
summary: List FAQs (admin)
|
|
||||||
tags:
|
|
||||||
- faqs
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Creates a new FAQ item
|
|
||||||
parameters:
|
|
||||||
- description: Create FAQ payload
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handlers.createFAQReq'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"201":
|
|
||||||
description: Created
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
summary: Create FAQ
|
|
||||||
tags:
|
|
||||||
- faqs
|
|
||||||
/api/v1/admin/faqs/{id}:
|
|
||||||
delete:
|
|
||||||
description: Deletes an FAQ item
|
|
||||||
parameters:
|
|
||||||
- description: FAQ ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"404":
|
|
||||||
description: Not Found
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
summary: Delete FAQ
|
|
||||||
tags:
|
|
||||||
- faqs
|
|
||||||
get:
|
|
||||||
description: Returns one FAQ regardless of status
|
|
||||||
parameters:
|
|
||||||
- description: FAQ ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"404":
|
|
||||||
description: Not Found
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
summary: Get FAQ by ID (admin)
|
|
||||||
tags:
|
|
||||||
- faqs
|
|
||||||
put:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Updates an existing FAQ item
|
|
||||||
parameters:
|
|
||||||
- description: FAQ ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: Update FAQ payload
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handlers.updateFAQReq'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"404":
|
|
||||||
description: Not Found
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
summary: Update FAQ
|
|
||||||
tags:
|
|
||||||
- faqs
|
|
||||||
/api/v1/admin/users/deletion-requests:
|
/api/v1/admin/users/deletion-requests:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -3374,67 +3121,6 @@ paths:
|
||||||
summary: Reorder modules within a unit
|
summary: Reorder modules within a unit
|
||||||
tags:
|
tags:
|
||||||
- exam-prep
|
- exam-prep
|
||||||
/api/v1/faqs:
|
|
||||||
get:
|
|
||||||
description: Returns active FAQs for public/help center usage
|
|
||||||
parameters:
|
|
||||||
- description: Filter by category
|
|
||||||
in: query
|
|
||||||
name: category
|
|
||||||
type: string
|
|
||||||
- description: Limit (default 50)
|
|
||||||
in: query
|
|
||||||
name: limit
|
|
||||||
type: integer
|
|
||||||
- description: Offset (default 0)
|
|
||||||
in: query
|
|
||||||
name: offset
|
|
||||||
type: integer
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
summary: List published FAQs
|
|
||||||
tags:
|
|
||||||
- faqs
|
|
||||||
/api/v1/faqs/{id}:
|
|
||||||
get:
|
|
||||||
description: Returns one active FAQ item
|
|
||||||
parameters:
|
|
||||||
- description: FAQ ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
"404":
|
|
||||||
description: Not Found
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
summary: Get published FAQ by ID
|
|
||||||
tags:
|
|
||||||
- faqs
|
|
||||||
/api/v1/files/audio:
|
/api/v1/files/audio:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
|
|
@ -57,34 +57,14 @@ func (q *Queries) ExamPrepDeleteCatalogCourse(ctx context.Context, id int64) err
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExamPrepGetCatalogCourseByID = `-- name: ExamPrepGetCatalogCourseByID :one
|
const ExamPrepGetCatalogCourseByID = `-- name: ExamPrepGetCatalogCourseByID :one
|
||||||
SELECT
|
SELECT id, name, description, thumbnail, sort_order, created_at, updated_at
|
||||||
c.id, c.name, c.description, c.thumbnail, c.sort_order, c.created_at, c.updated_at,
|
FROM exam_prep.catalog_courses
|
||||||
EXISTS (
|
WHERE id = $1
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
|
|
||||||
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
|
|
||||||
INNER JOIN exam_prep.units u ON u.id = m.unit_id
|
|
||||||
WHERE u.catalog_course_id = c.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM exam_prep.catalog_courses c
|
|
||||||
WHERE c.id = $1
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type ExamPrepGetCatalogCourseByIDRow struct {
|
func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (ExamPrepCatalogCourse, error) {
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (ExamPrepGetCatalogCourseByIDRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepGetCatalogCourseByID, id)
|
row := q.db.QueryRow(ctx, ExamPrepGetCatalogCourseByID, id)
|
||||||
var i ExamPrepGetCatalogCourseByIDRow
|
var i ExamPrepCatalogCourse
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
|
|
@ -93,7 +73,6 @@ func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (E
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.HasPractice,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -147,14 +126,6 @@ SELECT
|
||||||
COALESCE(cc.units_count, 0)::BIGINT AS units_count,
|
COALESCE(cc.units_count, 0)::BIGINT AS units_count,
|
||||||
COALESCE(cc.modules_count, 0)::BIGINT AS modules_count,
|
COALESCE(cc.modules_count, 0)::BIGINT AS modules_count,
|
||||||
COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_count,
|
COALESCE(cc.lessons_count, 0)::BIGINT AS lessons_count,
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
|
|
||||||
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
|
|
||||||
INNER JOIN exam_prep.units u ON u.id = m.unit_id
|
|
||||||
WHERE u.catalog_course_id = c.id
|
|
||||||
) AS has_practice,
|
|
||||||
c.created_at,
|
c.created_at,
|
||||||
c.updated_at
|
c.updated_at
|
||||||
FROM exam_prep.catalog_courses c
|
FROM exam_prep.catalog_courses c
|
||||||
|
|
@ -178,7 +149,6 @@ type ExamPrepListCatalogCoursesRow struct {
|
||||||
UnitsCount int64 `json:"units_count"`
|
UnitsCount int64 `json:"units_count"`
|
||||||
ModulesCount int64 `json:"modules_count"`
|
ModulesCount int64 `json:"modules_count"`
|
||||||
LessonsCount int64 `json:"lessons_count"`
|
LessonsCount int64 `json:"lessons_count"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +172,6 @@ func (q *Queries) ExamPrepListCatalogCourses(ctx context.Context, arg ExamPrepLi
|
||||||
&i.UnitsCount,
|
&i.UnitsCount,
|
||||||
&i.ModulesCount,
|
&i.ModulesCount,
|
||||||
&i.LessonsCount,
|
&i.LessonsCount,
|
||||||
&i.HasPractice,
|
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -71,33 +71,14 @@ func (q *Queries) ExamPrepDeleteUnitModuleLesson(ctx context.Context, id int64)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExamPrepGetUnitModuleLessonByID = `-- name: ExamPrepGetUnitModuleLessonByID :one
|
const ExamPrepGetUnitModuleLessonByID = `-- name: ExamPrepGetUnitModuleLessonByID :one
|
||||||
SELECT
|
SELECT id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at
|
||||||
l.id, l.unit_module_id, l.title, l.video_url, l.thumbnail, l.description, l.sort_order, l.created_at, l.updated_at,
|
FROM exam_prep.unit_module_lessons
|
||||||
EXISTS (
|
WHERE id = $1
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
WHERE p.unit_module_lesson_id = l.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM exam_prep.unit_module_lessons l
|
|
||||||
WHERE l.id = $1
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type ExamPrepGetUnitModuleLessonByIDRow struct {
|
func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64) (ExamPrepUnitModuleLesson, error) {
|
||||||
ID int64 `json:"id"`
|
|
||||||
UnitModuleID int64 `json:"unit_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
VideoUrl pgtype.Text `json:"video_url"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64) (ExamPrepGetUnitModuleLessonByIDRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepGetUnitModuleLessonByID, id)
|
row := q.db.QueryRow(ctx, ExamPrepGetUnitModuleLessonByID, id)
|
||||||
var i ExamPrepGetUnitModuleLessonByIDRow
|
var i ExamPrepUnitModuleLesson
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.UnitModuleID,
|
&i.UnitModuleID,
|
||||||
|
|
@ -108,7 +89,6 @@ func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64)
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.HasPractice,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -153,11 +133,6 @@ SELECT
|
||||||
l.thumbnail,
|
l.thumbnail,
|
||||||
l.description,
|
l.description,
|
||||||
l.sort_order,
|
l.sort_order,
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
WHERE p.unit_module_lesson_id = l.id
|
|
||||||
) AS has_practice,
|
|
||||||
l.created_at,
|
l.created_at,
|
||||||
l.updated_at
|
l.updated_at
|
||||||
FROM exam_prep.unit_module_lessons l
|
FROM exam_prep.unit_module_lessons l
|
||||||
|
|
@ -185,7 +160,6 @@ type ExamPrepListUnitModuleLessonsByUnitModuleIDRow struct {
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
@ -208,7 +182,6 @@ func (q *Queries) ExamPrepListUnitModuleLessonsByUnitModuleID(ctx context.Contex
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.HasPractice,
|
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -71,34 +71,14 @@ func (q *Queries) ExamPrepDeleteUnitModule(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExamPrepGetUnitModuleByID = `-- name: ExamPrepGetUnitModuleByID :one
|
const ExamPrepGetUnitModuleByID = `-- name: ExamPrepGetUnitModuleByID :one
|
||||||
SELECT
|
SELECT id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at
|
||||||
m.id, m.unit_id, m.name, m.description, m.thumbnail, m.icon, m.sort_order, m.created_at, m.updated_at,
|
FROM exam_prep.unit_modules
|
||||||
EXISTS (
|
WHERE id = $1
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
|
|
||||||
WHERE l.unit_module_id = m.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM exam_prep.unit_modules m
|
|
||||||
WHERE m.id = $1
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type ExamPrepGetUnitModuleByIDRow struct {
|
func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (ExamPrepUnitModule, error) {
|
||||||
ID int64 `json:"id"`
|
|
||||||
UnitID int64 `json:"unit_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Icon pgtype.Text `json:"icon"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (ExamPrepGetUnitModuleByIDRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepGetUnitModuleByID, id)
|
row := q.db.QueryRow(ctx, ExamPrepGetUnitModuleByID, id)
|
||||||
var i ExamPrepGetUnitModuleByIDRow
|
var i ExamPrepUnitModule
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.UnitID,
|
&i.UnitID,
|
||||||
|
|
@ -109,7 +89,6 @@ func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (Exam
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.HasPractice,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +145,6 @@ SELECT
|
||||||
m.sort_order,
|
m.sort_order,
|
||||||
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
|
COALESCE(mc.lessons_count, 0)::BIGINT AS lessons_count,
|
||||||
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
|
COALESCE(mc.practices_count, 0)::BIGINT AS practices_count,
|
||||||
(COALESCE(mc.practices_count, 0)::BIGINT > 0) AS has_practice,
|
|
||||||
m.created_at,
|
m.created_at,
|
||||||
m.updated_at
|
m.updated_at
|
||||||
FROM exam_prep.unit_modules m
|
FROM exam_prep.unit_modules m
|
||||||
|
|
@ -197,7 +175,6 @@ type ExamPrepListUnitModulesByUnitRow struct {
|
||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
LessonsCount int64 `json:"lessons_count"`
|
LessonsCount int64 `json:"lessons_count"`
|
||||||
PracticesCount int64 `json:"practices_count"`
|
PracticesCount int64 `json:"practices_count"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
@ -222,7 +199,6 @@ func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPre
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.LessonsCount,
|
&i.LessonsCount,
|
||||||
&i.PracticesCount,
|
&i.PracticesCount,
|
||||||
&i.HasPractice,
|
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -67,34 +67,14 @@ func (q *Queries) ExamPrepDeleteUnit(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExamPrepGetUnitByID = `-- name: ExamPrepGetUnitByID :one
|
const ExamPrepGetUnitByID = `-- name: ExamPrepGetUnitByID :one
|
||||||
SELECT
|
SELECT id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at
|
||||||
u.id, u.catalog_course_id, u.name, u.description, u.thumbnail, u.sort_order, u.created_at, u.updated_at,
|
FROM exam_prep.units
|
||||||
EXISTS (
|
WHERE id = $1
|
||||||
SELECT 1
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
INNER JOIN exam_prep.unit_module_lessons l ON l.id = p.unit_module_lesson_id
|
|
||||||
INNER JOIN exam_prep.unit_modules m ON m.id = l.unit_module_id
|
|
||||||
WHERE m.unit_id = u.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM exam_prep.units u
|
|
||||||
WHERE u.id = $1
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type ExamPrepGetUnitByIDRow struct {
|
func (q *Queries) ExamPrepGetUnitByID(ctx context.Context, id int64) (ExamPrepUnit, error) {
|
||||||
ID int64 `json:"id"`
|
|
||||||
CatalogCourseID int64 `json:"catalog_course_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepGetUnitByID(ctx context.Context, id int64) (ExamPrepGetUnitByIDRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepGetUnitByID, id)
|
row := q.db.QueryRow(ctx, ExamPrepGetUnitByID, id)
|
||||||
var i ExamPrepGetUnitByIDRow
|
var i ExamPrepUnit
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.CatalogCourseID,
|
&i.CatalogCourseID,
|
||||||
|
|
@ -104,7 +84,6 @@ func (q *Queries) ExamPrepGetUnitByID(ctx context.Context, id int64) (ExamPrepGe
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.HasPractice,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +142,6 @@ SELECT
|
||||||
COALESCE(uc.modules_count, 0)::BIGINT AS modules_count,
|
COALESCE(uc.modules_count, 0)::BIGINT AS modules_count,
|
||||||
COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_count,
|
COALESCE(uc.lessons_count, 0)::BIGINT AS lessons_count,
|
||||||
COALESCE(uc.practices_count, 0)::BIGINT AS practices_count,
|
COALESCE(uc.practices_count, 0)::BIGINT AS practices_count,
|
||||||
(COALESCE(uc.practices_count, 0)::BIGINT > 0) AS has_practice,
|
|
||||||
u.created_at,
|
u.created_at,
|
||||||
u.updated_at
|
u.updated_at
|
||||||
FROM exam_prep.units u
|
FROM exam_prep.units u
|
||||||
|
|
@ -194,7 +172,6 @@ type ExamPrepListUnitsByCatalogCourseRow struct {
|
||||||
ModulesCount int64 `json:"modules_count"`
|
ModulesCount int64 `json:"modules_count"`
|
||||||
LessonsCount int64 `json:"lessons_count"`
|
LessonsCount int64 `json:"lessons_count"`
|
||||||
PracticesCount int64 `json:"practices_count"`
|
PracticesCount int64 `json:"practices_count"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
@ -219,7 +196,6 @@ func (q *Queries) ExamPrepListUnitsByCatalogCourse(ctx context.Context, arg Exam
|
||||||
&i.ModulesCount,
|
&i.ModulesCount,
|
||||||
&i.LessonsCount,
|
&i.LessonsCount,
|
||||||
&i.PracticesCount,
|
&i.PracticesCount,
|
||||||
&i.HasPractice,
|
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -67,35 +67,14 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetCourseByID = `-- name: GetCourseByID :one
|
const GetCourseByID = `-- name: GetCourseByID :one
|
||||||
SELECT
|
SELECT id, program_id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
c.id, c.program_id, c.name, c.description, c.thumbnail, c.created_at, c.updated_at, c.sort_order,
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.course_id = c.id
|
|
||||||
AND p.module_id IS NULL
|
|
||||||
AND p.lesson_id IS NULL
|
|
||||||
) AS has_practice
|
|
||||||
FROM courses
|
FROM courses
|
||||||
c
|
WHERE id = $1
|
||||||
WHERE c.id = $1
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetCourseByIDRow struct {
|
func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
||||||
ID int64 `json:"id"`
|
|
||||||
ProgramID int64 `json:"program_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetCourseByID(ctx context.Context, id int64) (GetCourseByIDRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetCourseByID, id)
|
row := q.db.QueryRow(ctx, GetCourseByID, id)
|
||||||
var i GetCourseByIDRow
|
var i Course
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.ProgramID,
|
&i.ProgramID,
|
||||||
|
|
@ -105,7 +84,6 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (GetCourseByIDRow
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.HasPractice,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -177,14 +155,7 @@ SELECT
|
||||||
WHERE
|
WHERE
|
||||||
p.course_id = c.id
|
p.course_id = c.id
|
||||||
AND p.module_id IS NULL
|
AND p.module_id IS NULL
|
||||||
AND p.lesson_id IS NULL) AS practice_count,
|
AND p.lesson_id IS NULL) AS practice_count
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.course_id = c.id
|
|
||||||
AND p.module_id IS NULL
|
|
||||||
AND p.lesson_id IS NULL
|
|
||||||
) AS has_practice
|
|
||||||
FROM
|
FROM
|
||||||
courses c
|
courses c
|
||||||
WHERE
|
WHERE
|
||||||
|
|
@ -214,7 +185,6 @@ type ListCoursesByProgramIDRow struct {
|
||||||
ModuleCount int64 `json:"module_count"`
|
ModuleCount int64 `json:"module_count"`
|
||||||
LessonCount int64 `json:"lesson_count"`
|
LessonCount int64 `json:"lesson_count"`
|
||||||
PracticeCount int64 `json:"practice_count"`
|
PracticeCount int64 `json:"practice_count"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) {
|
func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) {
|
||||||
|
|
@ -239,7 +209,6 @@ func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByP
|
||||||
&i.ModuleCount,
|
&i.ModuleCount,
|
||||||
&i.LessonCount,
|
&i.LessonCount,
|
||||||
&i.PracticeCount,
|
&i.PracticeCount,
|
||||||
&i.HasPractice,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,34 +71,14 @@ func (q *Queries) DeleteLesson(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetLessonByID = `-- name: GetLessonByID :one
|
const GetLessonByID = `-- name: GetLessonByID :one
|
||||||
SELECT
|
SELECT id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order
|
||||||
l.id, l.module_id, l.title, l.video_url, l.thumbnail, l.description, l.created_at, l.updated_at, l.sort_order,
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.lesson_id = l.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM lessons
|
FROM lessons
|
||||||
l
|
WHERE id = $1
|
||||||
WHERE l.id = $1
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetLessonByIDRow struct {
|
func (q *Queries) GetLessonByID(ctx context.Context, id int64) (Lesson, error) {
|
||||||
ID int64 `json:"id"`
|
|
||||||
ModuleID int64 `json:"module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
VideoUrl pgtype.Text `json:"video_url"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetLessonByID(ctx context.Context, id int64) (GetLessonByIDRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetLessonByID, id)
|
row := q.db.QueryRow(ctx, GetLessonByID, id)
|
||||||
var i GetLessonByIDRow
|
var i Lesson
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.ModuleID,
|
&i.ModuleID,
|
||||||
|
|
@ -109,7 +89,6 @@ func (q *Queries) GetLessonByID(ctx context.Context, id int64) (GetLessonByIDRow
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.HasPractice,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -125,12 +104,7 @@ SELECT
|
||||||
l.description,
|
l.description,
|
||||||
l.sort_order,
|
l.sort_order,
|
||||||
l.created_at,
|
l.created_at,
|
||||||
l.updated_at,
|
l.updated_at
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.lesson_id = l.id
|
|
||||||
) AS has_practice
|
|
||||||
FROM
|
FROM
|
||||||
lessons l
|
lessons l
|
||||||
WHERE
|
WHERE
|
||||||
|
|
@ -159,7 +133,6 @@ type ListLessonsByModuleIDRow struct {
|
||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByModuleIDParams) ([]ListLessonsByModuleIDRow, error) {
|
func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByModuleIDParams) ([]ListLessonsByModuleIDRow, error) {
|
||||||
|
|
@ -182,7 +155,6 @@ func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByMo
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.HasPractice,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,35 +71,14 @@ func (q *Queries) DeleteModule(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetModuleByID = `-- name: GetModuleByID :one
|
const GetModuleByID = `-- name: GetModuleByID :one
|
||||||
SELECT
|
SELECT id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order
|
||||||
m.id, m.program_id, m.course_id, m.name, m.description, m.icon, m.created_at, m.updated_at, m.sort_order,
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.module_id = m.id
|
|
||||||
AND p.lesson_id IS NULL
|
|
||||||
) AS has_practice
|
|
||||||
FROM modules
|
FROM modules
|
||||||
m
|
WHERE id = $1
|
||||||
WHERE m.id = $1
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetModuleByIDRow struct {
|
func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) {
|
||||||
ID int64 `json:"id"`
|
|
||||||
ProgramID int64 `json:"program_id"`
|
|
||||||
CourseID int64 `json:"course_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Icon pgtype.Text `json:"icon"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetModuleByID(ctx context.Context, id int64) (GetModuleByIDRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetModuleByID, id)
|
row := q.db.QueryRow(ctx, GetModuleByID, id)
|
||||||
var i GetModuleByIDRow
|
var i Module
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.ProgramID,
|
&i.ProgramID,
|
||||||
|
|
@ -110,7 +89,6 @@ func (q *Queries) GetModuleByID(ctx context.Context, id int64) (GetModuleByIDRow
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.HasPractice,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -157,13 +135,7 @@ SELECT
|
||||||
m.icon,
|
m.icon,
|
||||||
m.sort_order,
|
m.sort_order,
|
||||||
m.created_at,
|
m.created_at,
|
||||||
m.updated_at,
|
m.updated_at
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM lms_practices p
|
|
||||||
WHERE p.module_id = m.id
|
|
||||||
AND p.lesson_id IS NULL
|
|
||||||
) AS has_practice
|
|
||||||
FROM
|
FROM
|
||||||
modules m
|
modules m
|
||||||
WHERE
|
WHERE
|
||||||
|
|
@ -194,7 +166,6 @@ type ListModulesByProgramAndCourseRow struct {
|
||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListModulesByProgramAndCourseParams) ([]ListModulesByProgramAndCourseRow, error) {
|
func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListModulesByProgramAndCourseParams) ([]ListModulesByProgramAndCourseRow, error) {
|
||||||
|
|
@ -222,7 +193,6 @@ func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListMod
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.HasPractice,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ type Course struct {
|
||||||
ModuleCount int `json:"module_count"`
|
ModuleCount int `json:"module_count"`
|
||||||
LessonCount int `json:"lesson_count"`
|
LessonCount int `json:"lesson_count"`
|
||||||
PracticeCount int `json:"practice_count"`
|
PracticeCount int `json:"practice_count"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
Access *LMSEntityAccess `json:"access,omitempty"`
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ type ExamPrepCatalogCourse struct {
|
||||||
UnitsCount *int64 `json:"units_count,omitempty"`
|
UnitsCount *int64 `json:"units_count,omitempty"`
|
||||||
ModulesCount *int64 `json:"modules_count,omitempty"`
|
ModulesCount *int64 `json:"modules_count,omitempty"`
|
||||||
LessonsCount *int64 `json:"lessons_count,omitempty"`
|
LessonsCount *int64 `json:"lessons_count,omitempty"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ type ExamPrepLesson struct {
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ type ExamPrepModule struct {
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
LessonsCount *int64 `json:"lessons_count,omitempty"`
|
LessonsCount *int64 `json:"lessons_count,omitempty"`
|
||||||
PracticesCount *int64 `json:"practices_count,omitempty"`
|
PracticesCount *int64 `json:"practices_count,omitempty"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ type ExamPrepUnit struct {
|
||||||
ModulesCount *int64 `json:"modules_count,omitempty"`
|
ModulesCount *int64 `json:"modules_count,omitempty"`
|
||||||
LessonsCount *int64 `json:"lessons_count,omitempty"`
|
LessonsCount *int64 `json:"lessons_count,omitempty"`
|
||||||
PracticesCount *int64 `json:"practices_count,omitempty"`
|
PracticesCount *int64 `json:"practices_count,omitempty"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
const (
|
|
||||||
FAQStatusActive = "ACTIVE"
|
|
||||||
FAQStatusInactive = "INACTIVE"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FAQ struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Question string `json:"question"`
|
|
||||||
Answer string `json:"answer"`
|
|
||||||
Category *string `json:"category,omitempty"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateFAQInput struct {
|
|
||||||
Question string
|
|
||||||
Answer string
|
|
||||||
Category *string
|
|
||||||
DisplayOrder *int32
|
|
||||||
Status *string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateFAQInput struct {
|
|
||||||
Question *string
|
|
||||||
Answer *string
|
|
||||||
Category *string
|
|
||||||
DisplayOrder *int32
|
|
||||||
Status *string
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,6 @@ type Lesson struct {
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
Access *LMSEntityAccess `json:"access,omitempty"`
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ type Module struct {
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Icon *string `json:"icon,omitempty"`
|
Icon *string `json:"icon,omitempty"`
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
HasPractice bool `json:"has_practice"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
Access *LMSEntityAccess `json:"access,omitempty"`
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -221,11 +221,7 @@ func ResolveRuntimeQuestionTypeFromDefinition(key string, responseKinds []string
|
||||||
return "TRUE_FALSE"
|
return "TRUE_FALSE"
|
||||||
}
|
}
|
||||||
|
|
||||||
hasNonAuxiliary := false
|
|
||||||
for _, kind := range normalizeKindList(responseKinds) {
|
for _, kind := range normalizeKindList(responseKinds) {
|
||||||
if _, aux := responseKindsAuxiliary[kind]; !aux {
|
|
||||||
hasNonAuxiliary = true
|
|
||||||
}
|
|
||||||
switch kind {
|
switch kind {
|
||||||
case string(ResponseAudioResponse):
|
case string(ResponseAudioResponse):
|
||||||
return "AUDIO"
|
return "AUDIO"
|
||||||
|
|
@ -241,11 +237,6 @@ func ResolveRuntimeQuestionTypeFromDefinition(key string, responseKinds []string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builder-native response kinds should still be persistable/executable as DYNAMIC.
|
|
||||||
if hasNonAuxiliary {
|
|
||||||
return "DYNAMIC"
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,13 +86,6 @@ func TestResolveRuntimeQuestionTypeFromDefinition_skipsAuxiliaryKinds(t *testing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveRuntimeQuestionTypeFromDefinition_builderNativeKinds(t *testing.T) {
|
|
||||||
got := ResolveRuntimeQuestionTypeFromDefinition("dynamic_builder", []string{"OPTION", "ANSWER_TIMER"})
|
|
||||||
if got != "DYNAMIC" {
|
|
||||||
t.Fatalf("expected DYNAMIC, got %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidatePersistableQuestionTypeDefinition_unmappable(t *testing.T) {
|
func TestValidatePersistableQuestionTypeDefinition_unmappable(t *testing.T) {
|
||||||
err := ValidatePersistableQuestionTypeDefinition("custom", []string{"ANSWER_TIMER"})
|
err := ValidatePersistableQuestionTypeDefinition("custom", []string{"ANSWER_TIMER"})
|
||||||
if err == nil || !strings.Contains(err.Error(), "unable to map definition") {
|
if err == nil || !strings.Contains(err.Error(), "unable to map definition") {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package ports
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FAQStore interface {
|
|
||||||
CreateFAQ(ctx context.Context, input domain.CreateFAQInput) (domain.FAQ, error)
|
|
||||||
UpdateFAQ(ctx context.Context, id int64, input domain.UpdateFAQInput) (domain.FAQ, error)
|
|
||||||
GetFAQByID(ctx context.Context, id int64, includeInactive bool) (domain.FAQ, error)
|
|
||||||
ListFAQs(ctx context.Context, status *string, category *string, limit int32, offset int32) ([]domain.FAQ, int64, error)
|
|
||||||
DeleteFAQ(ctx context.Context, id int64) error
|
|
||||||
}
|
|
||||||
|
|
@ -47,17 +47,7 @@ func (s *Store) GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (dom
|
||||||
}
|
}
|
||||||
return domain.ExamPrepCatalogCourse{}, err
|
return domain.ExamPrepCatalogCourse{}, err
|
||||||
}
|
}
|
||||||
out := examPrepCatalogCourseToDomain(dbgen.ExamPrepCatalogCourse{
|
return examPrepCatalogCourseToDomain(c), nil
|
||||||
ID: c.ID,
|
|
||||||
Name: c.Name,
|
|
||||||
Description: c.Description,
|
|
||||||
Thumbnail: c.Thumbnail,
|
|
||||||
SortOrder: c.SortOrder,
|
|
||||||
CreatedAt: c.CreatedAt,
|
|
||||||
UpdatedAt: c.UpdatedAt,
|
|
||||||
})
|
|
||||||
out.HasPractice = c.HasPractice
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error) {
|
func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error) {
|
||||||
|
|
@ -89,7 +79,6 @@ func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, limit, offset in
|
||||||
item.UnitsCount = &r.UnitsCount
|
item.UnitsCount = &r.UnitsCount
|
||||||
item.ModulesCount = &r.ModulesCount
|
item.ModulesCount = &r.ModulesCount
|
||||||
item.LessonsCount = &r.LessonsCount
|
item.LessonsCount = &r.LessonsCount
|
||||||
item.HasPractice = r.HasPractice
|
|
||||||
out = append(out, item)
|
out = append(out, item)
|
||||||
}
|
}
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
|
|
|
||||||
|
|
@ -51,19 +51,7 @@ func (s *Store) GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) (
|
||||||
}
|
}
|
||||||
return domain.ExamPrepLesson{}, err
|
return domain.ExamPrepLesson{}, err
|
||||||
}
|
}
|
||||||
out := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{
|
return examPrepLessonToDomain(l), nil
|
||||||
ID: l.ID,
|
|
||||||
UnitModuleID: l.UnitModuleID,
|
|
||||||
Title: l.Title,
|
|
||||||
VideoUrl: l.VideoUrl,
|
|
||||||
Thumbnail: l.Thumbnail,
|
|
||||||
Description: l.Description,
|
|
||||||
SortOrder: l.SortOrder,
|
|
||||||
CreatedAt: l.CreatedAt,
|
|
||||||
UpdatedAt: l.UpdatedAt,
|
|
||||||
})
|
|
||||||
out.HasPractice = l.HasPractice
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, unitModuleID int64, limit, offset int32) ([]domain.ExamPrepLesson, int64, error) {
|
func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, unitModuleID int64, limit, offset int32) ([]domain.ExamPrepLesson, int64, error) {
|
||||||
|
|
@ -84,7 +72,7 @@ func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context,
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
total = r.TotalCount
|
total = r.TotalCount
|
||||||
}
|
}
|
||||||
item := examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{
|
out = append(out, examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
UnitModuleID: r.UnitModuleID,
|
UnitModuleID: r.UnitModuleID,
|
||||||
Title: r.Title,
|
Title: r.Title,
|
||||||
|
|
@ -94,9 +82,7 @@ func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context,
|
||||||
SortOrder: r.SortOrder,
|
SortOrder: r.SortOrder,
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
UpdatedAt: r.UpdatedAt,
|
UpdatedAt: r.UpdatedAt,
|
||||||
})
|
}))
|
||||||
item.HasPractice = r.HasPractice
|
|
||||||
out = append(out, item)
|
|
||||||
}
|
}
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,19 +51,7 @@ func (s *Store) GetExamPrepUnitModuleByID(ctx context.Context, id int64) (domain
|
||||||
}
|
}
|
||||||
return domain.ExamPrepModule{}, err
|
return domain.ExamPrepModule{}, err
|
||||||
}
|
}
|
||||||
out := examPrepModuleToDomain(dbgen.ExamPrepUnitModule{
|
return examPrepModuleToDomain(m), nil
|
||||||
ID: m.ID,
|
|
||||||
UnitID: m.UnitID,
|
|
||||||
Name: m.Name,
|
|
||||||
Description: m.Description,
|
|
||||||
Thumbnail: m.Thumbnail,
|
|
||||||
Icon: m.Icon,
|
|
||||||
SortOrder: m.SortOrder,
|
|
||||||
CreatedAt: m.CreatedAt,
|
|
||||||
UpdatedAt: m.UpdatedAt,
|
|
||||||
})
|
|
||||||
out.HasPractice = m.HasPractice
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64, limit, offset int32) ([]domain.ExamPrepModule, int64, error) {
|
func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64, limit, offset int32) ([]domain.ExamPrepModule, int64, error) {
|
||||||
|
|
@ -97,7 +85,6 @@ func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64,
|
||||||
})
|
})
|
||||||
item.LessonsCount = &r.LessonsCount
|
item.LessonsCount = &r.LessonsCount
|
||||||
item.PracticesCount = &r.PracticesCount
|
item.PracticesCount = &r.PracticesCount
|
||||||
item.HasPractice = r.HasPractice
|
|
||||||
out = append(out, item)
|
out = append(out, item)
|
||||||
}
|
}
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
|
|
|
||||||
|
|
@ -49,18 +49,7 @@ func (s *Store) GetExamPrepUnitByID(ctx context.Context, id int64) (domain.ExamP
|
||||||
}
|
}
|
||||||
return domain.ExamPrepUnit{}, err
|
return domain.ExamPrepUnit{}, err
|
||||||
}
|
}
|
||||||
out := examPrepUnitToDomain(dbgen.ExamPrepUnit{
|
return examPrepUnitToDomain(u), nil
|
||||||
ID: u.ID,
|
|
||||||
CatalogCourseID: u.CatalogCourseID,
|
|
||||||
Name: u.Name,
|
|
||||||
Description: u.Description,
|
|
||||||
Thumbnail: u.Thumbnail,
|
|
||||||
SortOrder: u.SortOrder,
|
|
||||||
CreatedAt: u.CreatedAt,
|
|
||||||
UpdatedAt: u.UpdatedAt,
|
|
||||||
})
|
|
||||||
out.HasPractice = u.HasPractice
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, limit, offset int32) ([]domain.ExamPrepUnit, int64, error) {
|
func (s *Store) ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, limit, offset int32) ([]domain.ExamPrepUnit, int64, error) {
|
||||||
|
|
@ -94,7 +83,6 @@ func (s *Store) ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCou
|
||||||
item.ModulesCount = &r.ModulesCount
|
item.ModulesCount = &r.ModulesCount
|
||||||
item.LessonsCount = &r.LessonsCount
|
item.LessonsCount = &r.LessonsCount
|
||||||
item.PracticesCount = &r.PracticesCount
|
item.PracticesCount = &r.PracticesCount
|
||||||
item.HasPractice = r.HasPractice
|
|
||||||
out = append(out, item)
|
out = append(out, item)
|
||||||
}
|
}
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
|
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"Yimaru-Backend/internal/ports"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewFAQStore(s *Store) ports.FAQStore { return s }
|
|
||||||
|
|
||||||
func faqToDomain(
|
|
||||||
id int64,
|
|
||||||
question string,
|
|
||||||
answer string,
|
|
||||||
category pgtype.Text,
|
|
||||||
displayOrder int32,
|
|
||||||
status string,
|
|
||||||
createdAt pgtype.Timestamptz,
|
|
||||||
updatedAt pgtype.Timestamptz,
|
|
||||||
) domain.FAQ {
|
|
||||||
return domain.FAQ{
|
|
||||||
ID: id,
|
|
||||||
Question: question,
|
|
||||||
Answer: answer,
|
|
||||||
Category: fromPgText(category),
|
|
||||||
DisplayOrder: displayOrder,
|
|
||||||
Status: status,
|
|
||||||
CreatedAt: createdAt.Time,
|
|
||||||
UpdatedAt: timePtr(updatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateFAQ(ctx context.Context, input domain.CreateFAQInput) (domain.FAQ, error) {
|
|
||||||
displayOrder := int32(0)
|
|
||||||
if input.DisplayOrder != nil {
|
|
||||||
displayOrder = *input.DisplayOrder
|
|
||||||
}
|
|
||||||
status := domain.FAQStatusActive
|
|
||||||
if input.Status != nil {
|
|
||||||
status = *input.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
row := s.conn.QueryRow(ctx, `
|
|
||||||
INSERT INTO faqs (question, answer, category, display_order, status)
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING id, question, answer, category, display_order, status, created_at, updated_at
|
|
||||||
`, input.Question, input.Answer, toPgText(input.Category), displayOrder, status)
|
|
||||||
|
|
||||||
var (
|
|
||||||
id int64
|
|
||||||
question string
|
|
||||||
answer string
|
|
||||||
category pgtype.Text
|
|
||||||
orderVal int32
|
|
||||||
faqStatus string
|
|
||||||
createdAt pgtype.Timestamptz
|
|
||||||
updatedAt pgtype.Timestamptz
|
|
||||||
)
|
|
||||||
if err := row.Scan(&id, &question, &answer, &category, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
|
|
||||||
return domain.FAQ{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return faqToDomain(id, question, answer, category, orderVal, faqStatus, createdAt, updatedAt), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateFAQ(ctx context.Context, id int64, input domain.UpdateFAQInput) (domain.FAQ, error) {
|
|
||||||
categorySet := input.Category != nil
|
|
||||||
var categoryValue pgtype.Text
|
|
||||||
if categorySet {
|
|
||||||
if *input.Category == "" {
|
|
||||||
categoryValue = pgtype.Text{Valid: false}
|
|
||||||
} else {
|
|
||||||
categoryValue = pgtype.Text{String: *input.Category, Valid: true}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
row := s.conn.QueryRow(ctx, `
|
|
||||||
UPDATE faqs
|
|
||||||
SET question = COALESCE($2, question),
|
|
||||||
answer = COALESCE($3, answer),
|
|
||||||
category = CASE WHEN $4::boolean THEN $5::text ELSE category END,
|
|
||||||
display_order = COALESCE($6, display_order),
|
|
||||||
status = COALESCE($7, status),
|
|
||||||
updated_at = NOW()
|
|
||||||
WHERE id = $1
|
|
||||||
RETURNING id, question, answer, category, display_order, status, created_at, updated_at
|
|
||||||
`,
|
|
||||||
id,
|
|
||||||
input.Question,
|
|
||||||
input.Answer,
|
|
||||||
categorySet,
|
|
||||||
categoryValue,
|
|
||||||
input.DisplayOrder,
|
|
||||||
input.Status,
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
faqID int64
|
|
||||||
question string
|
|
||||||
answer string
|
|
||||||
rowCategory pgtype.Text
|
|
||||||
orderVal int32
|
|
||||||
faqStatus string
|
|
||||||
createdAt pgtype.Timestamptz
|
|
||||||
updatedAt pgtype.Timestamptz
|
|
||||||
)
|
|
||||||
if err := row.Scan(&faqID, &question, &answer, &rowCategory, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
|
|
||||||
return domain.FAQ{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return faqToDomain(faqID, question, answer, rowCategory, orderVal, faqStatus, createdAt, updatedAt), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetFAQByID(ctx context.Context, id int64, includeInactive bool) (domain.FAQ, error) {
|
|
||||||
row := s.conn.QueryRow(ctx, `
|
|
||||||
SELECT id, question, answer, category, display_order, status, created_at, updated_at
|
|
||||||
FROM faqs
|
|
||||||
WHERE id = $1
|
|
||||||
AND ($2::boolean = TRUE OR status = 'ACTIVE')
|
|
||||||
`, id, includeInactive)
|
|
||||||
|
|
||||||
var (
|
|
||||||
faqID int64
|
|
||||||
question string
|
|
||||||
answer string
|
|
||||||
category pgtype.Text
|
|
||||||
orderVal int32
|
|
||||||
faqStatus string
|
|
||||||
createdAt pgtype.Timestamptz
|
|
||||||
updatedAt pgtype.Timestamptz
|
|
||||||
)
|
|
||||||
if err := row.Scan(&faqID, &question, &answer, &category, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
|
|
||||||
return domain.FAQ{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return faqToDomain(faqID, question, answer, category, orderVal, faqStatus, createdAt, updatedAt), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListFAQs(ctx context.Context, status *string, category *string, limit int32, offset int32) ([]domain.FAQ, int64, error) {
|
|
||||||
rows, err := s.conn.Query(ctx, `
|
|
||||||
SELECT id, question, answer, category, display_order, status, created_at, updated_at
|
|
||||||
FROM faqs
|
|
||||||
WHERE ($1::text IS NULL OR status = $1)
|
|
||||||
AND ($2::text IS NULL OR category = $2)
|
|
||||||
ORDER BY display_order ASC, id ASC
|
|
||||||
LIMIT $3 OFFSET $4
|
|
||||||
`, toPgText(status), toPgText(category), limit, offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
faqs := make([]domain.FAQ, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var (
|
|
||||||
faqID int64
|
|
||||||
question string
|
|
||||||
answer string
|
|
||||||
rowCategory pgtype.Text
|
|
||||||
orderVal int32
|
|
||||||
faqStatus string
|
|
||||||
createdAt pgtype.Timestamptz
|
|
||||||
updatedAt pgtype.Timestamptz
|
|
||||||
)
|
|
||||||
if err := rows.Scan(&faqID, &question, &answer, &rowCategory, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
faqs = append(faqs, faqToDomain(faqID, question, answer, rowCategory, orderVal, faqStatus, createdAt, updatedAt))
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalCount int64
|
|
||||||
if err := s.conn.QueryRow(ctx, `
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM faqs
|
|
||||||
WHERE ($1::text IS NULL OR status = $1)
|
|
||||||
AND ($2::text IS NULL OR category = $2)
|
|
||||||
`, toPgText(status), toPgText(category)).Scan(&totalCount); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return faqs, totalCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteFAQ(ctx context.Context, id int64) error {
|
|
||||||
cmd, err := s.conn.Exec(ctx, `DELETE FROM faqs WHERE id = $1`, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if cmd.RowsAffected() == 0 {
|
|
||||||
return pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -53,18 +53,7 @@ func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, err
|
||||||
}
|
}
|
||||||
return domain.Course{}, err
|
return domain.Course{}, err
|
||||||
}
|
}
|
||||||
out := courseToDomain(dbgen.Course{
|
return courseToDomain(c), nil
|
||||||
ID: c.ID,
|
|
||||||
ProgramID: c.ProgramID,
|
|
||||||
Name: c.Name,
|
|
||||||
Description: c.Description,
|
|
||||||
Thumbnail: c.Thumbnail,
|
|
||||||
SortOrder: c.SortOrder,
|
|
||||||
CreatedAt: c.CreatedAt,
|
|
||||||
UpdatedAt: c.UpdatedAt,
|
|
||||||
})
|
|
||||||
out.HasPractice = c.HasPractice
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error) {
|
func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error) {
|
||||||
|
|
@ -98,7 +87,6 @@ func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, lim
|
||||||
co.ModuleCount = int(r.ModuleCount)
|
co.ModuleCount = int(r.ModuleCount)
|
||||||
co.LessonCount = int(r.LessonCount)
|
co.LessonCount = int(r.LessonCount)
|
||||||
co.PracticeCount = int(r.PracticeCount)
|
co.PracticeCount = int(r.PracticeCount)
|
||||||
co.HasPractice = r.HasPractice
|
|
||||||
out = append(out, co)
|
out = append(out, co)
|
||||||
}
|
}
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
|
|
|
||||||
|
|
@ -51,19 +51,7 @@ func (s *Store) GetLessonByID(ctx context.Context, id int64) (domain.Lesson, err
|
||||||
}
|
}
|
||||||
return domain.Lesson{}, err
|
return domain.Lesson{}, err
|
||||||
}
|
}
|
||||||
out := lessonToDomain(dbgen.Lesson{
|
return lessonToDomain(l), nil
|
||||||
ID: l.ID,
|
|
||||||
ModuleID: l.ModuleID,
|
|
||||||
Title: l.Title,
|
|
||||||
VideoUrl: l.VideoUrl,
|
|
||||||
Thumbnail: l.Thumbnail,
|
|
||||||
Description: l.Description,
|
|
||||||
SortOrder: l.SortOrder,
|
|
||||||
CreatedAt: l.CreatedAt,
|
|
||||||
UpdatedAt: l.UpdatedAt,
|
|
||||||
})
|
|
||||||
out.HasPractice = l.HasPractice
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error) {
|
func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error) {
|
||||||
|
|
@ -84,7 +72,7 @@ func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
total = r.TotalCount
|
total = r.TotalCount
|
||||||
}
|
}
|
||||||
lesson := lessonToDomain(dbgen.Lesson{
|
out = append(out, lessonToDomain(dbgen.Lesson{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
ModuleID: r.ModuleID,
|
ModuleID: r.ModuleID,
|
||||||
Title: r.Title,
|
Title: r.Title,
|
||||||
|
|
@ -94,9 +82,7 @@ func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
UpdatedAt: r.UpdatedAt,
|
UpdatedAt: r.UpdatedAt,
|
||||||
SortOrder: r.SortOrder,
|
SortOrder: r.SortOrder,
|
||||||
})
|
}))
|
||||||
lesson.HasPractice = r.HasPractice
|
|
||||||
out = append(out, lesson)
|
|
||||||
}
|
}
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,19 +55,7 @@ func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, err
|
||||||
}
|
}
|
||||||
return domain.Module{}, err
|
return domain.Module{}, err
|
||||||
}
|
}
|
||||||
out := moduleToDomain(dbgen.Module{
|
return moduleToDomain(m), nil
|
||||||
ID: m.ID,
|
|
||||||
ProgramID: m.ProgramID,
|
|
||||||
CourseID: m.CourseID,
|
|
||||||
Name: m.Name,
|
|
||||||
Description: m.Description,
|
|
||||||
Icon: m.Icon,
|
|
||||||
SortOrder: m.SortOrder,
|
|
||||||
CreatedAt: m.CreatedAt,
|
|
||||||
UpdatedAt: m.UpdatedAt,
|
|
||||||
})
|
|
||||||
out.HasPractice = m.HasPractice
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error) {
|
func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error) {
|
||||||
|
|
@ -89,7 +77,7 @@ func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, co
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
total = r.TotalCount
|
total = r.TotalCount
|
||||||
}
|
}
|
||||||
mod := moduleToDomain(dbgen.Module{
|
out = append(out, moduleToDomain(dbgen.Module{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
ProgramID: r.ProgramID,
|
ProgramID: r.ProgramID,
|
||||||
CourseID: r.CourseID,
|
CourseID: r.CourseID,
|
||||||
|
|
@ -99,9 +87,7 @@ func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, co
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
UpdatedAt: r.UpdatedAt,
|
UpdatedAt: r.UpdatedAt,
|
||||||
SortOrder: r.SortOrder,
|
SortOrder: r.SortOrder,
|
||||||
})
|
}))
|
||||||
mod.HasPractice = r.HasPractice
|
|
||||||
out = append(out, mod)
|
|
||||||
}
|
}
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
package faqs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"Yimaru-Backend/internal/ports"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
faqStore ports.FAQStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService(faqStore ports.FAQStore) *Service {
|
|
||||||
return &Service{faqStore: faqStore}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeFAQStatus(status *string) (string, error) {
|
|
||||||
if status == nil || strings.TrimSpace(*status) == "" {
|
|
||||||
return domain.FAQStatusActive, nil
|
|
||||||
}
|
|
||||||
value := strings.ToUpper(strings.TrimSpace(*status))
|
|
||||||
switch value {
|
|
||||||
case domain.FAQStatusActive, domain.FAQStatusInactive:
|
|
||||||
return value, nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("status must be one of %s, %s", domain.FAQStatusActive, domain.FAQStatusInactive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateFAQ(ctx context.Context, input domain.CreateFAQInput) (domain.FAQ, error) {
|
|
||||||
input.Question = strings.TrimSpace(input.Question)
|
|
||||||
input.Answer = strings.TrimSpace(input.Answer)
|
|
||||||
if input.Question == "" {
|
|
||||||
return domain.FAQ{}, fmt.Errorf("question is required")
|
|
||||||
}
|
|
||||||
if input.Answer == "" {
|
|
||||||
return domain.FAQ{}, fmt.Errorf("answer is required")
|
|
||||||
}
|
|
||||||
if input.Category != nil {
|
|
||||||
c := strings.TrimSpace(*input.Category)
|
|
||||||
if c == "" {
|
|
||||||
input.Category = nil
|
|
||||||
} else {
|
|
||||||
input.Category = &c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
status, err := normalizeFAQStatus(input.Status)
|
|
||||||
if err != nil {
|
|
||||||
return domain.FAQ{}, err
|
|
||||||
}
|
|
||||||
input.Status = &status
|
|
||||||
return s.faqStore.CreateFAQ(ctx, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) UpdateFAQ(ctx context.Context, id int64, input domain.UpdateFAQInput) (domain.FAQ, error) {
|
|
||||||
if id <= 0 {
|
|
||||||
return domain.FAQ{}, fmt.Errorf("invalid faq id")
|
|
||||||
}
|
|
||||||
if input.Question != nil {
|
|
||||||
trimmed := strings.TrimSpace(*input.Question)
|
|
||||||
if trimmed == "" {
|
|
||||||
return domain.FAQ{}, fmt.Errorf("question cannot be empty")
|
|
||||||
}
|
|
||||||
input.Question = &trimmed
|
|
||||||
}
|
|
||||||
if input.Answer != nil {
|
|
||||||
trimmed := strings.TrimSpace(*input.Answer)
|
|
||||||
if trimmed == "" {
|
|
||||||
return domain.FAQ{}, fmt.Errorf("answer cannot be empty")
|
|
||||||
}
|
|
||||||
input.Answer = &trimmed
|
|
||||||
}
|
|
||||||
if input.Category != nil {
|
|
||||||
c := strings.TrimSpace(*input.Category)
|
|
||||||
input.Category = &c
|
|
||||||
}
|
|
||||||
if input.Status != nil {
|
|
||||||
status, err := normalizeFAQStatus(input.Status)
|
|
||||||
if err != nil {
|
|
||||||
return domain.FAQ{}, err
|
|
||||||
}
|
|
||||||
input.Status = &status
|
|
||||||
}
|
|
||||||
return s.faqStore.UpdateFAQ(ctx, id, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetFAQByID(ctx context.Context, id int64, includeInactive bool) (domain.FAQ, error) {
|
|
||||||
if id <= 0 {
|
|
||||||
return domain.FAQ{}, fmt.Errorf("invalid faq id")
|
|
||||||
}
|
|
||||||
return s.faqStore.GetFAQByID(ctx, id, includeInactive)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListFAQs(ctx context.Context, status *string, category *string, limit int32, offset int32) ([]domain.FAQ, int64, error) {
|
|
||||||
if status != nil {
|
|
||||||
normalized, err := normalizeFAQStatus(status)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
status = &normalized
|
|
||||||
}
|
|
||||||
if category != nil {
|
|
||||||
trimmed := strings.TrimSpace(*category)
|
|
||||||
if trimmed == "" {
|
|
||||||
category = nil
|
|
||||||
} else {
|
|
||||||
category = &trimmed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = 20
|
|
||||||
}
|
|
||||||
if limit > 200 {
|
|
||||||
limit = 200
|
|
||||||
}
|
|
||||||
if offset < 0 {
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
return s.faqStore.ListFAQs(ctx, status, category, limit, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) DeleteFAQ(ctx context.Context, id int64) error {
|
|
||||||
if id <= 0 {
|
|
||||||
return fmt.Errorf("invalid faq id")
|
|
||||||
}
|
|
||||||
return s.faqStore.DeleteFAQ(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
@ -12,7 +12,7 @@ func (s *Service) SendEmail(ctx context.Context, receiverEmail, message string,
|
||||||
func (s *Service) SendEmailWithAttachments(ctx context.Context, receiverEmail, message string, messageHTML string, subject string, attachments []*resend.Attachment) error {
|
func (s *Service) SendEmailWithAttachments(ctx context.Context, receiverEmail, message string, messageHTML string, subject string, attachments []*resend.Attachment) error {
|
||||||
apiKey := s.config.ResendApiKey
|
apiKey := s.config.ResendApiKey
|
||||||
client := resend.NewClient(apiKey)
|
client := resend.NewClient(apiKey)
|
||||||
formattedSenderEmail := "Yimaru - Academy <" + s.config.ResendSenderEmail + ">"
|
formattedSenderEmail := "Y <" + s.config.ResendSenderEmail + ">"
|
||||||
params := &resend.SendEmailRequest{
|
params := &resend.SendEmailRequest{
|
||||||
From: formattedSenderEmail,
|
From: formattedSenderEmail,
|
||||||
To: []string{receiverEmail},
|
To: []string{receiverEmail},
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
// "errors"
|
// "errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
@ -23,11 +22,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// "github.com/segmentio/kafka-go"
|
// "github.com/segmentio/kafka-go"
|
||||||
|
"go.uber.org/zap"
|
||||||
firebase "firebase.google.com/go/v4"
|
firebase "firebase.google.com/go/v4"
|
||||||
"firebase.google.com/go/v4/messaging"
|
"firebase.google.com/go/v4/messaging"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/resend/resend-go/v2"
|
"github.com/resend/resend-go/v2"
|
||||||
"go.uber.org/zap"
|
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
// "github.com/redis/go-redis/v9"
|
// "github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
@ -69,10 +68,6 @@ func New(
|
||||||
config: cfg,
|
config: cfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
mongoLogger.Info("FCM_SERVICE_ACCOUNT_KEY value at startup",
|
|
||||||
zap.String("fcm_service_account_key", cfg.FCMServiceAccountKey),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize FCM client if service account key is provided
|
// Initialize FCM client if service account key is provided
|
||||||
if cfg.FCMServiceAccountKey != "" {
|
if cfg.FCMServiceAccountKey != "" {
|
||||||
if err := svc.initFCMClient(); err != nil {
|
if err := svc.initFCMClient(); err != nil {
|
||||||
|
|
@ -95,25 +90,12 @@ func (s *Service) initFCMClient() error {
|
||||||
|
|
||||||
// Prepare client options; if a service account JSON string is provided, use it.
|
// Prepare client options; if a service account JSON string is provided, use it.
|
||||||
var opts []option.ClientOption
|
var opts []option.ClientOption
|
||||||
var fbConfig *firebase.Config
|
|
||||||
if s.config.FCMServiceAccountKey != "" {
|
if s.config.FCMServiceAccountKey != "" {
|
||||||
var sa struct {
|
|
||||||
ProjectID string `json:"project_id"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(s.config.FCMServiceAccountKey), &sa); err != nil {
|
|
||||||
return fmt.Errorf("invalid FCM_SERVICE_ACCOUNT_KEY JSON: %w", err)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(sa.ProjectID) == "" {
|
|
||||||
return fmt.Errorf("FCM_SERVICE_ACCOUNT_KEY is missing project_id")
|
|
||||||
}
|
|
||||||
fbConfig = &firebase.Config{
|
|
||||||
ProjectID: strings.TrimSpace(sa.ProjectID),
|
|
||||||
}
|
|
||||||
opts = append(opts, option.WithCredentialsJSON([]byte(s.config.FCMServiceAccountKey)))
|
opts = append(opts, option.WithCredentialsJSON([]byte(s.config.FCMServiceAccountKey)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Firebase app
|
// Initialize Firebase app
|
||||||
app, err := firebase.NewApp(ctx, fbConfig, opts...)
|
app, err := firebase.NewApp(ctx, nil, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize Firebase app: %w", err)
|
return fmt.Errorf("failed to initialize Firebase app: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -128,19 +110,6 @@ func (s *Service) initFCMClient() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensureFCMClient() error {
|
|
||||||
if s.fcmClient != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(s.config.FCMServiceAccountKey) == "" {
|
|
||||||
return fmt.Errorf("FCM_SERVICE_ACCOUNT_KEY is empty")
|
|
||||||
}
|
|
||||||
if err := s.initFCMClient(); err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize FCM client: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendAfroMessageSMSTemp(
|
func (s *Service) SendAfroMessageSMSTemp(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
receiverPhone string,
|
receiverPhone string,
|
||||||
|
|
@ -566,8 +535,8 @@ func (s *Service) SendNotificationEmail(ctx context.Context, recipientID int64,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SendPushNotification(ctx context.Context, notification *domain.Notification) error {
|
func (s *Service) SendPushNotification(ctx context.Context, notification *domain.Notification) error {
|
||||||
if err := s.ensureFCMClient(); err != nil {
|
if s.fcmClient == nil {
|
||||||
return err
|
return fmt.Errorf("FCM client not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user device tokens
|
// Get user device tokens
|
||||||
|
|
@ -649,8 +618,8 @@ func (s *Service) MessengerSvc() *messenger.Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SendBulkPushNotification(ctx context.Context, userIDs []int64, notification *domain.Notification) (sent int, failed int, err error) {
|
func (s *Service) SendBulkPushNotification(ctx context.Context, userIDs []int64, notification *domain.Notification) (sent int, failed int, err error) {
|
||||||
if err := s.ensureFCMClient(); err != nil {
|
if s.fcmClient == nil {
|
||||||
return 0, 0, err
|
return 0, 0, fmt.Errorf("FCM client not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all device tokens for the given users
|
// Collect all device tokens for the given users
|
||||||
|
|
|
||||||
|
|
@ -232,13 +232,6 @@ var AllPermissions = []domain.PermissionSeed{
|
||||||
{Key: "settings.get", Name: "Get Setting", Description: "Get setting by key", GroupName: "Settings"},
|
{Key: "settings.get", Name: "Get Setting", Description: "Get setting by key", GroupName: "Settings"},
|
||||||
{Key: "settings.update", Name: "Update Settings", Description: "Update settings", GroupName: "Settings"},
|
{Key: "settings.update", Name: "Update Settings", Description: "Update settings", GroupName: "Settings"},
|
||||||
|
|
||||||
// FAQs
|
|
||||||
{Key: "faqs.create", Name: "Create FAQ", Description: "Create a FAQ item", GroupName: "FAQs"},
|
|
||||||
{Key: "faqs.list", Name: "List FAQs (Admin)", Description: "List FAQs for admin management", GroupName: "FAQs"},
|
|
||||||
{Key: "faqs.get", Name: "Get FAQ (Admin)", Description: "Get FAQ by ID for admin management", GroupName: "FAQs"},
|
|
||||||
{Key: "faqs.update", Name: "Update FAQ", Description: "Update a FAQ item", GroupName: "FAQs"},
|
|
||||||
{Key: "faqs.delete", Name: "Delete FAQ", Description: "Delete a FAQ item", GroupName: "FAQs"},
|
|
||||||
|
|
||||||
// Analytics
|
// Analytics
|
||||||
{Key: "analytics.dashboard", Name: "View Dashboard", Description: "View analytics dashboard", GroupName: "Analytics"},
|
{Key: "analytics.dashboard", Name: "View Dashboard", Description: "View analytics dashboard", GroupName: "Analytics"},
|
||||||
|
|
||||||
|
|
@ -373,9 +366,6 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
// Settings (previously SuperAdminOnly, now accessible to ADMIN too)
|
// Settings (previously SuperAdminOnly, now accessible to ADMIN too)
|
||||||
"settings.list", "settings.get", "settings.update",
|
"settings.list", "settings.get", "settings.update",
|
||||||
|
|
||||||
// FAQs
|
|
||||||
"faqs.create", "faqs.list", "faqs.get", "faqs.update", "faqs.delete",
|
|
||||||
|
|
||||||
// Analytics (previously OnlyAdminAndAbove)
|
// Analytics (previously OnlyAdminAndAbove)
|
||||||
"analytics.dashboard",
|
"analytics.dashboard",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,14 @@ import (
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
||||||
|
minioservice "Yimaru-Backend/internal/services/minio"
|
||||||
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||||
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
"Yimaru-Backend/internal/services/courses"
|
"Yimaru-Backend/internal/services/courses"
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
"Yimaru-Backend/internal/services/examprep"
|
||||||
"Yimaru-Backend/internal/services/faqs"
|
|
||||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
|
||||||
"Yimaru-Backend/internal/services/lessons"
|
"Yimaru-Backend/internal/services/lessons"
|
||||||
"Yimaru-Backend/internal/services/lmsprogress"
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
minioservice "Yimaru-Backend/internal/services/minio"
|
|
||||||
"Yimaru-Backend/internal/services/modules"
|
"Yimaru-Backend/internal/services/modules"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
|
||||||
"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"
|
||||||
|
|
@ -45,17 +44,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
assessmentSvc *assessment.Service
|
assessmentSvc *assessment.Service
|
||||||
questionsSvc *questions.Service
|
questionsSvc *questions.Service
|
||||||
faqSvc *faqs.Service
|
examPrepSvc *examprep.Service
|
||||||
examPrepSvc *examprep.Service
|
programSvc *programs.Service
|
||||||
programSvc *programs.Service
|
courseSvc *courses.Service
|
||||||
courseSvc *courses.Service
|
moduleSvc *modules.Service
|
||||||
moduleSvc *modules.Service
|
lessonSvc *lessons.Service
|
||||||
lessonSvc *lessons.Service
|
lmsProgressSvc *lmsprogress.Service
|
||||||
lmsProgressSvc *lmsprogress.Service
|
practiceSvc *practices.Service
|
||||||
practiceSvc *practices.Service
|
subscriptionsSvc *subscriptions.Service
|
||||||
subscriptionsSvc *subscriptions.Service
|
|
||||||
arifpaySvc *arifpay.ArifpayService
|
arifpaySvc *arifpay.ArifpayService
|
||||||
issueReportingSvc *issuereporting.Service
|
issueReportingSvc *issuereporting.Service
|
||||||
vimeoSvc *vimeoservice.Service
|
vimeoSvc *vimeoservice.Service
|
||||||
|
|
@ -77,16 +75,15 @@ type App struct {
|
||||||
validator *customvalidator.CustomValidator
|
validator *customvalidator.CustomValidator
|
||||||
JwtConfig jwtutil.JwtConfig
|
JwtConfig jwtutil.JwtConfig
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
mongoLoggerSvc *zap.Logger
|
mongoLoggerSvc *zap.Logger
|
||||||
analyticsDB *dbgen.Queries
|
analyticsDB *dbgen.Queries
|
||||||
rbacSvc *rbacservice.Service
|
rbacSvc *rbacservice.Service
|
||||||
stopPurgeWorker context.CancelFunc
|
stopPurgeWorker context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(
|
func NewApp(
|
||||||
assessmentSvc *assessment.Service,
|
assessmentSvc *assessment.Service,
|
||||||
questionsSvc *questions.Service,
|
questionsSvc *questions.Service,
|
||||||
faqSvc *faqs.Service,
|
|
||||||
examPrepSvc *examprep.Service,
|
examPrepSvc *examprep.Service,
|
||||||
programSvc *programs.Service,
|
programSvc *programs.Service,
|
||||||
courseSvc *courses.Service,
|
courseSvc *courses.Service,
|
||||||
|
|
@ -137,7 +134,6 @@ func NewApp(
|
||||||
s := &App{
|
s := &App{
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
faqSvc: faqSvc,
|
|
||||||
examPrepSvc: examPrepSvc,
|
examPrepSvc: examPrepSvc,
|
||||||
programSvc: programSvc,
|
programSvc: programSvc,
|
||||||
courseSvc: courseSvc,
|
courseSvc: courseSvc,
|
||||||
|
|
|
||||||
|
|
@ -1,396 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
type createFAQReq struct {
|
|
||||||
Question string `json:"question" validate:"required"`
|
|
||||||
Answer string `json:"answer" validate:"required"`
|
|
||||||
Category *string `json:"category"`
|
|
||||||
DisplayOrder *int32 `json:"display_order"`
|
|
||||||
Status *string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateFAQReq struct {
|
|
||||||
Question *string `json:"question"`
|
|
||||||
Answer *string `json:"answer"`
|
|
||||||
Category *string `json:"category"`
|
|
||||||
DisplayOrder *int32 `json:"display_order"`
|
|
||||||
Status *string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type faqRes struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Question string `json:"question"`
|
|
||||||
Answer string `json:"answer"`
|
|
||||||
Category *string `json:"category,omitempty"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
|
||||||
UpdatedAt *string `json:"updated_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type listFAQsRes struct {
|
|
||||||
FAQs []faqRes `json:"faqs"`
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapFAQToRes(f domain.FAQ) faqRes {
|
|
||||||
var updatedAt *string
|
|
||||||
if f.UpdatedAt != nil {
|
|
||||||
value := f.UpdatedAt.String()
|
|
||||||
updatedAt = &value
|
|
||||||
}
|
|
||||||
return faqRes{
|
|
||||||
ID: f.ID,
|
|
||||||
Question: f.Question,
|
|
||||||
Answer: f.Answer,
|
|
||||||
Category: f.Category,
|
|
||||||
DisplayOrder: f.DisplayOrder,
|
|
||||||
Status: f.Status,
|
|
||||||
CreatedAt: f.CreatedAt.String(),
|
|
||||||
UpdatedAt: updatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPublicFAQs godoc
|
|
||||||
// @Summary List published FAQs
|
|
||||||
// @Description Returns active FAQs for public/help center usage
|
|
||||||
// @Tags faqs
|
|
||||||
// @Produce json
|
|
||||||
// @Param category query string false "Filter by category"
|
|
||||||
// @Param limit query int false "Limit (default 50)"
|
|
||||||
// @Param offset query int false "Offset (default 0)"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/faqs [get]
|
|
||||||
func (h *Handler) ListPublicFAQs(c *fiber.Ctx) error {
|
|
||||||
category := strings.TrimSpace(c.Query("category"))
|
|
||||||
var categoryPtr *string
|
|
||||||
if category != "" {
|
|
||||||
categoryPtr = &category
|
|
||||||
}
|
|
||||||
|
|
||||||
limit, err := strconv.Atoi(c.Query("limit", "50"))
|
|
||||||
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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
status := domain.FAQStatusActive
|
|
||||||
faqs, total, err := h.faqSvc.ListFAQs(c.Context(), &status, categoryPtr, int32(limit), int32(offset))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to list FAQs",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make([]faqRes, 0, len(faqs))
|
|
||||||
for _, f := range faqs {
|
|
||||||
out = append(out, mapFAQToRes(f))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "FAQs retrieved successfully",
|
|
||||||
Data: listFAQsRes{
|
|
||||||
FAQs: out,
|
|
||||||
TotalCount: total,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPublicFAQByID godoc
|
|
||||||
// @Summary Get published FAQ by ID
|
|
||||||
// @Description Returns one active FAQ item
|
|
||||||
// @Tags faqs
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path int true "FAQ ID"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 404 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/faqs/{id} [get]
|
|
||||||
func (h *Handler) GetPublicFAQByID(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid FAQ ID",
|
|
||||||
Error: "id must be a positive integer",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
faq, err := h.faqSvc.GetFAQByID(c.Context(), id, false)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "FAQ not found",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to get FAQ",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "FAQ retrieved successfully",
|
|
||||||
Data: mapFAQToRes(faq),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListFAQsAdmin godoc
|
|
||||||
// @Summary List FAQs (admin)
|
|
||||||
// @Description Returns FAQs for admin management with status/category filtering
|
|
||||||
// @Tags faqs
|
|
||||||
// @Produce json
|
|
||||||
// @Param status query string false "ACTIVE or INACTIVE"
|
|
||||||
// @Param category query string false "Filter by category"
|
|
||||||
// @Param limit query int false "Limit (default 20)"
|
|
||||||
// @Param offset query int false "Offset (default 0)"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/admin/faqs [get]
|
|
||||||
func (h *Handler) ListFAQsAdmin(c *fiber.Ctx) error {
|
|
||||||
status := strings.ToUpper(strings.TrimSpace(c.Query("status")))
|
|
||||||
var statusPtr *string
|
|
||||||
if status != "" {
|
|
||||||
statusPtr = &status
|
|
||||||
}
|
|
||||||
category := strings.TrimSpace(c.Query("category"))
|
|
||||||
var categoryPtr *string
|
|
||||||
if category != "" {
|
|
||||||
categoryPtr = &category
|
|
||||||
}
|
|
||||||
|
|
||||||
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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
faqs, total, err := h.faqSvc.ListFAQs(c.Context(), statusPtr, categoryPtr, int32(limit), int32(offset))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to list FAQs",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make([]faqRes, 0, len(faqs))
|
|
||||||
for _, f := range faqs {
|
|
||||||
out = append(out, mapFAQToRes(f))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "FAQs retrieved successfully",
|
|
||||||
Data: listFAQsRes{
|
|
||||||
FAQs: out,
|
|
||||||
TotalCount: total,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFAQByIDAdmin godoc
|
|
||||||
// @Summary Get FAQ by ID (admin)
|
|
||||||
// @Description Returns one FAQ regardless of status
|
|
||||||
// @Tags faqs
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path int true "FAQ ID"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 404 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/admin/faqs/{id} [get]
|
|
||||||
func (h *Handler) GetFAQByIDAdmin(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid FAQ ID",
|
|
||||||
Error: "id must be a positive integer",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
faq, err := h.faqSvc.GetFAQByID(c.Context(), id, true)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "FAQ not found",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to get FAQ",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "FAQ retrieved successfully",
|
|
||||||
Data: mapFAQToRes(faq),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateFAQ godoc
|
|
||||||
// @Summary Create FAQ
|
|
||||||
// @Description Creates a new FAQ item
|
|
||||||
// @Tags faqs
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param body body createFAQReq true "Create FAQ payload"
|
|
||||||
// @Success 201 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/admin/faqs [post]
|
|
||||||
func (h *Handler) CreateFAQ(c *fiber.Ctx) error {
|
|
||||||
var req createFAQReq
|
|
||||||
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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
faq, err := h.faqSvc.CreateFAQ(c.Context(), domain.CreateFAQInput{
|
|
||||||
Question: req.Question,
|
|
||||||
Answer: req.Answer,
|
|
||||||
Category: req.Category,
|
|
||||||
DisplayOrder: req.DisplayOrder,
|
|
||||||
Status: req.Status,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to create FAQ",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
||||||
Message: "FAQ created successfully",
|
|
||||||
Data: mapFAQToRes(faq),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateFAQ godoc
|
|
||||||
// @Summary Update FAQ
|
|
||||||
// @Description Updates an existing FAQ item
|
|
||||||
// @Tags faqs
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path int true "FAQ ID"
|
|
||||||
// @Param body body updateFAQReq true "Update FAQ payload"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 404 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/admin/faqs/{id} [put]
|
|
||||||
func (h *Handler) UpdateFAQ(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid FAQ ID",
|
|
||||||
Error: "id must be a positive integer",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var req updateFAQReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
faq, err := h.faqSvc.UpdateFAQ(c.Context(), id, domain.UpdateFAQInput{
|
|
||||||
Question: req.Question,
|
|
||||||
Answer: req.Answer,
|
|
||||||
Category: req.Category,
|
|
||||||
DisplayOrder: req.DisplayOrder,
|
|
||||||
Status: req.Status,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "FAQ not found",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to update FAQ",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "FAQ updated successfully",
|
|
||||||
Data: mapFAQToRes(faq),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteFAQ godoc
|
|
||||||
// @Summary Delete FAQ
|
|
||||||
// @Description Deletes an FAQ item
|
|
||||||
// @Tags faqs
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path int true "FAQ ID"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 404 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/admin/faqs/{id} [delete]
|
|
||||||
func (h *Handler) DeleteFAQ(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil || id <= 0 {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid FAQ ID",
|
|
||||||
Error: "id must be a positive integer",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.faqSvc.DeleteFAQ(c.Context(), id); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "FAQ not found",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to delete FAQ",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "FAQ deleted successfully",
|
|
||||||
Data: fiber.Map{"id": id},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -11,20 +11,19 @@ import (
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
||||||
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||||
|
minioservice "Yimaru-Backend/internal/services/minio"
|
||||||
|
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
||||||
|
rbacservice "Yimaru-Backend/internal/services/rbac"
|
||||||
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
"Yimaru-Backend/internal/services/courses"
|
"Yimaru-Backend/internal/services/courses"
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
"Yimaru-Backend/internal/services/examprep"
|
||||||
"Yimaru-Backend/internal/services/faqs"
|
|
||||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
|
||||||
"Yimaru-Backend/internal/services/lessons"
|
"Yimaru-Backend/internal/services/lessons"
|
||||||
"Yimaru-Backend/internal/services/lmsprogress"
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
minioservice "Yimaru-Backend/internal/services/minio"
|
|
||||||
"Yimaru-Backend/internal/services/modules"
|
"Yimaru-Backend/internal/services/modules"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
|
||||||
"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"
|
||||||
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
|
||||||
rbacservice "Yimaru-Backend/internal/services/rbac"
|
|
||||||
"Yimaru-Backend/internal/services/recommendation"
|
"Yimaru-Backend/internal/services/recommendation"
|
||||||
"Yimaru-Backend/internal/services/subscriptions"
|
"Yimaru-Backend/internal/services/subscriptions"
|
||||||
"Yimaru-Backend/internal/services/team"
|
"Yimaru-Backend/internal/services/team"
|
||||||
|
|
@ -44,17 +43,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
assessmentSvc *assessment.Service
|
assessmentSvc *assessment.Service
|
||||||
questionsSvc *questions.Service
|
questionsSvc *questions.Service
|
||||||
faqSvc *faqs.Service
|
examPrepSvc *examprep.Service
|
||||||
examPrepSvc *examprep.Service
|
programSvc *programs.Service
|
||||||
programSvc *programs.Service
|
courseSvc *courses.Service
|
||||||
courseSvc *courses.Service
|
moduleSvc *modules.Service
|
||||||
moduleSvc *modules.Service
|
lessonSvc *lessons.Service
|
||||||
lessonSvc *lessons.Service
|
lmsProgressSvc *lmsprogress.Service
|
||||||
lmsProgressSvc *lmsprogress.Service
|
practiceSvc *practices.Service
|
||||||
practiceSvc *practices.Service
|
subscriptionsSvc *subscriptions.Service
|
||||||
subscriptionsSvc *subscriptions.Service
|
|
||||||
arifpaySvc *arifpay.ArifpayService
|
arifpaySvc *arifpay.ArifpayService
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
settingSvc *settings.Service
|
settingSvc *settings.Service
|
||||||
|
|
@ -81,7 +79,6 @@ type Handler struct {
|
||||||
func New(
|
func New(
|
||||||
assessmentSvc *assessment.Service,
|
assessmentSvc *assessment.Service,
|
||||||
questionsSvc *questions.Service,
|
questionsSvc *questions.Service,
|
||||||
faqSvc *faqs.Service,
|
|
||||||
examPrepSvc *examprep.Service,
|
examPrepSvc *examprep.Service,
|
||||||
programSvc *programs.Service,
|
programSvc *programs.Service,
|
||||||
courseSvc *courses.Service,
|
courseSvc *courses.Service,
|
||||||
|
|
@ -115,7 +112,6 @@ func New(
|
||||||
return &Handler{
|
return &Handler{
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
faqSvc: faqSvc,
|
|
||||||
examPrepSvc: examPrepSvc,
|
examPrepSvc: examPrepSvc,
|
||||||
programSvc: programSvc,
|
programSvc: programSvc,
|
||||||
courseSvc: courseSvc,
|
courseSvc: courseSvc,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
|
@ -17,7 +16,6 @@ import (
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
|
||||||
"github.com/resend/resend-go/v2"
|
"github.com/resend/resend-go/v2"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
@ -113,7 +111,7 @@ type hijackResponseWriter struct {
|
||||||
h http.Header
|
h http.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *hijackResponseWriter) Header() http.Header { return w.h }
|
func (w *hijackResponseWriter) Header() http.Header { return w.h }
|
||||||
func (w *hijackResponseWriter) WriteHeader(statusCode int) {}
|
func (w *hijackResponseWriter) WriteHeader(statusCode int) {}
|
||||||
func (w *hijackResponseWriter) Write(b []byte) (int, error) { return w.conn.Write(b) }
|
func (w *hijackResponseWriter) Write(b []byte) (int, error) { return w.conn.Write(b) }
|
||||||
func (w *hijackResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
func (w *hijackResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
|
@ -664,12 +662,6 @@ func (h *Handler) RegisterDeviceToken(c *fiber.Ctx) error {
|
||||||
)
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Validation failed",
|
|
||||||
Error: firstValidationError(valErrs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, ok := c.Locals("user_id").(int64)
|
userID, ok := c.Locals("user_id").(int64)
|
||||||
if !ok || userID == 0 {
|
if !ok || userID == 0 {
|
||||||
|
|
@ -681,14 +673,6 @@ func (h *Handler) RegisterDeviceToken(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.userSvc.RegisterDevice(c.Context(), userID, req.DeviceToken, req.Platform); err != nil {
|
if err := h.userSvc.RegisterDevice(c.Context(), userID, req.DeviceToken, req.Platform); err != nil {
|
||||||
var pgErr *pgconn.PgError
|
|
||||||
if errors.As(err, &pgErr) && pgErr.Code == "23503" && pgErr.ConstraintName == "devices_user_fk" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid authenticated user",
|
|
||||||
Error: "authenticated user does not exist in users table",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
h.mongoLoggerSvc.Error("[NotificationHandler.RegisterDeviceToken] Failed to register device token",
|
h.mongoLoggerSvc.Error("[NotificationHandler.RegisterDeviceToken] Failed to register device token",
|
||||||
zap.Int64("userID", userID),
|
zap.Int64("userID", userID),
|
||||||
zap.String("platform", req.Platform),
|
zap.String("platform", req.Platform),
|
||||||
|
|
@ -727,10 +711,6 @@ func (h *Handler) RegisterDeviceToken(c *fiber.Ctx) error {
|
||||||
func (h *Handler) SendTestPushNotification(c *fiber.Ctx) error {
|
func (h *Handler) SendTestPushNotification(c *fiber.Ctx) error {
|
||||||
title := c.FormValue("title", "Test Push Notification")
|
title := c.FormValue("title", "Test Push Notification")
|
||||||
message := c.FormValue("message", "This is a test push notification from Yimaru Backend")
|
message := c.FormValue("message", "This is a test push notification from Yimaru Backend")
|
||||||
h.mongoLoggerSvc.Info("FCM_SERVICE_ACCOUNT_KEY value during test-push call",
|
|
||||||
zap.String("fcm_service_account_key", h.Cfg.FCMServiceAccountKey),
|
|
||||||
zap.String("db_url", h.Cfg.DbUrl),
|
|
||||||
)
|
|
||||||
|
|
||||||
userID, ok := c.Locals("user_id").(int64)
|
userID, ok := c.Locals("user_id").(int64)
|
||||||
if !ok || userID == 0 {
|
if !ok || userID == 0 {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -135,27 +132,6 @@ func (h *Handler) CreateQuestionTypeDefinition(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := c.Locals("user_id").(int64)
|
|
||||||
actorRole := string(c.Locals("role").(domain.Role))
|
|
||||||
ip := c.IP()
|
|
||||||
ua := c.Get("User-Agent")
|
|
||||||
meta, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"question_type_definition_id": def.ID,
|
|
||||||
"key": def.Key,
|
|
||||||
})
|
|
||||||
go h.activityLogSvc.RecordAction(
|
|
||||||
context.Background(),
|
|
||||||
&actorID,
|
|
||||||
&actorRole,
|
|
||||||
domain.ActionQuestionCreated,
|
|
||||||
domain.ResourceQuestion,
|
|
||||||
&def.ID,
|
|
||||||
"Created question type definition: "+def.DisplayName,
|
|
||||||
meta,
|
|
||||||
&ip,
|
|
||||||
&ua,
|
|
||||||
)
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Question type definition created",
|
Message: "Question type definition created",
|
||||||
Data: def,
|
Data: def,
|
||||||
|
|
@ -275,26 +251,6 @@ func (h *Handler) UpdateQuestionTypeDefinition(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := c.Locals("user_id").(int64)
|
|
||||||
actorRole := string(c.Locals("role").(domain.Role))
|
|
||||||
ip := c.IP()
|
|
||||||
ua := c.Get("User-Agent")
|
|
||||||
meta, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"question_type_definition_id": id,
|
|
||||||
})
|
|
||||||
go h.activityLogSvc.RecordAction(
|
|
||||||
context.Background(),
|
|
||||||
&actorID,
|
|
||||||
&actorRole,
|
|
||||||
domain.ActionQuestionUpdated,
|
|
||||||
domain.ResourceQuestion,
|
|
||||||
&id,
|
|
||||||
fmt.Sprintf("Updated question type definition ID: %d", id),
|
|
||||||
meta,
|
|
||||||
&ip,
|
|
||||||
&ua,
|
|
||||||
)
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Question type definition updated",
|
Message: "Question type definition updated",
|
||||||
Data: fiber.Map{"id": id},
|
Data: fiber.Map{"id": id},
|
||||||
|
|
@ -326,26 +282,6 @@ func (h *Handler) DeleteQuestionTypeDefinition(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := c.Locals("user_id").(int64)
|
|
||||||
actorRole := string(c.Locals("role").(domain.Role))
|
|
||||||
ip := c.IP()
|
|
||||||
ua := c.Get("User-Agent")
|
|
||||||
meta, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"question_type_definition_id": id,
|
|
||||||
})
|
|
||||||
go h.activityLogSvc.RecordAction(
|
|
||||||
context.Background(),
|
|
||||||
&actorID,
|
|
||||||
&actorRole,
|
|
||||||
domain.ActionQuestionDeleted,
|
|
||||||
domain.ResourceQuestion,
|
|
||||||
&id,
|
|
||||||
fmt.Sprintf("Deleted question type definition ID: %d", id),
|
|
||||||
meta,
|
|
||||||
&ip,
|
|
||||||
&ua,
|
|
||||||
)
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Question type definition deleted",
|
Message: "Question type definition deleted",
|
||||||
Data: fiber.Map{"id": id},
|
Data: fiber.Map{"id": id},
|
||||||
|
|
|
||||||
|
|
@ -731,7 +731,7 @@ func isSequenceGatedPractice(set domain.QuestionSet) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
ot := strings.ToUpper(strings.TrimSpace(*set.OwnerType))
|
ot := strings.ToUpper(strings.TrimSpace(*set.OwnerType))
|
||||||
return ot == "SUB_COURSE" || ot == "SUB_MODULE" || ot == "COURSE" || ot == "MODULE" || ot == "LESSON"
|
return ot == "SUB_COURSE" || ot == "SUB_MODULE"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) enforcePracticeSequenceForStudent(c *fiber.Ctx, set domain.QuestionSet) error {
|
func (h *Handler) enforcePracticeSequenceForStudent(c *fiber.Ctx, set domain.QuestionSet) error {
|
||||||
|
|
@ -1299,17 +1299,6 @@ func (h *Handler) AddQuestionToSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := c.Locals("user_id").(int64)
|
|
||||||
actorRole := string(c.Locals("role").(domain.Role))
|
|
||||||
ip := c.IP()
|
|
||||||
ua := c.Get("User-Agent")
|
|
||||||
meta, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"question_set_id": setID,
|
|
||||||
"question_id": req.QuestionID,
|
|
||||||
"display_order": req.DisplayOrder,
|
|
||||||
})
|
|
||||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &setID, fmt.Sprintf("Added question %d to question set %d", req.QuestionID, setID), meta, &ip, &ua)
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Question added to set successfully",
|
Message: "Question added to set successfully",
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
|
|
@ -1539,7 +1528,7 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
userID := c.Locals("user_id").(int64)
|
userID := c.Locals("user_id").(int64)
|
||||||
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
setID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid practice ID",
|
Message: "Invalid practice ID",
|
||||||
|
|
@ -1547,23 +1536,12 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
set, err := h.questionsSvc.GetQuestionSetByID(c.Context(), id)
|
set, err := h.questionsSvc.GetQuestionSetByID(c.Context(), setID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Backward/UX compatibility: accept either question_set.id or lms_practices.id.
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
practice, practiceErr := h.practiceSvc.GetByID(c.Context(), id)
|
Message: "Practice not found",
|
||||||
if practiceErr != nil {
|
Error: err.Error(),
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
})
|
||||||
Message: "Practice not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
set, err = h.questionsSvc.GetQuestionSetByID(c.Context(), practice.QuestionSetID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Practice not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !isSequenceGatedPractice(set) || !strings.EqualFold(set.Status, "PUBLISHED") {
|
if !isSequenceGatedPractice(set) || !strings.EqualFold(set.Status, "PUBLISHED") {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
|
@ -1627,16 +1605,6 @@ func (h *Handler) RemoveQuestionFromSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := c.Locals("user_id").(int64)
|
|
||||||
actorRole := string(c.Locals("role").(domain.Role))
|
|
||||||
ip := c.IP()
|
|
||||||
ua := c.Get("User-Agent")
|
|
||||||
meta, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"question_set_id": setID,
|
|
||||||
"question_id": questionID,
|
|
||||||
})
|
|
||||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &setID, fmt.Sprintf("Removed question %d from question set %d", questionID, setID), meta, &ip, &ua)
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Question removed from set successfully",
|
Message: "Question removed from set successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -1694,17 +1662,6 @@ func (h *Handler) UpdateQuestionOrderInSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := c.Locals("user_id").(int64)
|
|
||||||
actorRole := string(c.Locals("role").(domain.Role))
|
|
||||||
ip := c.IP()
|
|
||||||
ua := c.Get("User-Agent")
|
|
||||||
meta, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"question_set_id": setID,
|
|
||||||
"question_id": questionID,
|
|
||||||
"display_order": req.DisplayOrder,
|
|
||||||
})
|
|
||||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &setID, fmt.Sprintf("Updated question %d display_order to %d in set %d", questionID, req.DisplayOrder, setID), meta, &ip, &ua)
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Question order updated successfully",
|
Message: "Question order updated successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -1812,17 +1769,6 @@ func (h *Handler) AddUserPersonaToQuestionSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := c.Locals("user_id").(int64)
|
|
||||||
actorRole := string(c.Locals("role").(domain.Role))
|
|
||||||
ip := c.IP()
|
|
||||||
ua := c.Get("User-Agent")
|
|
||||||
meta, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"question_set_id": setID,
|
|
||||||
"user_id": req.UserID,
|
|
||||||
"display_order": req.DisplayOrder,
|
|
||||||
})
|
|
||||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &setID, fmt.Sprintf("Added persona user %d to question set %d", req.UserID, setID), meta, &ip, &ua)
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Persona added to question set successfully",
|
Message: "Persona added to question set successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -1866,16 +1812,6 @@ func (h *Handler) RemoveUserPersonaFromQuestionSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := c.Locals("user_id").(int64)
|
|
||||||
actorRole := string(c.Locals("role").(domain.Role))
|
|
||||||
ip := c.IP()
|
|
||||||
ua := c.Get("User-Agent")
|
|
||||||
meta, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"question_set_id": setID,
|
|
||||||
"user_id": userID,
|
|
||||||
})
|
|
||||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &setID, fmt.Sprintf("Removed persona user %d from question set %d", userID, setID), meta, &ip, &ua)
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Persona removed from question set successfully",
|
Message: "Persona removed from question set successfully",
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ func (a *App) initAppRoutes() {
|
||||||
h := handlers.New(
|
h := handlers.New(
|
||||||
a.assessmentSvc,
|
a.assessmentSvc,
|
||||||
a.questionsSvc,
|
a.questionsSvc,
|
||||||
a.faqSvc,
|
|
||||||
a.examPrepSvc,
|
a.examPrepSvc,
|
||||||
a.programSvc,
|
a.programSvc,
|
||||||
a.courseSvc,
|
a.courseSvc,
|
||||||
|
|
@ -141,7 +140,6 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Delete("/modules/:id", a.authMiddleware, a.RequirePermission("modules.delete"), h.DeleteModule)
|
groupV1.Delete("/modules/:id", a.authMiddleware, a.RequirePermission("modules.delete"), h.DeleteModule)
|
||||||
groupV1.Get("/lessons/:id/practices", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("practices.list"), h.ListPracticesByLesson)
|
groupV1.Get("/lessons/:id/practices", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("practices.list"), h.ListPracticesByLesson)
|
||||||
groupV1.Post("/lessons/:id/complete", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lessons.complete"), h.CompleteLesson)
|
groupV1.Post("/lessons/:id/complete", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lessons.complete"), h.CompleteLesson)
|
||||||
groupV1.Post("/progress/practices/:id/complete", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("progress.complete"), h.CompletePractice)
|
|
||||||
groupV1.Get("/lessons/:id", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lessons.get"), h.GetLesson)
|
groupV1.Get("/lessons/:id", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lessons.get"), h.GetLesson)
|
||||||
groupV1.Put("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.update"), h.UpdateLesson)
|
groupV1.Put("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.update"), h.UpdateLesson)
|
||||||
groupV1.Delete("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.delete"), h.DeleteLesson)
|
groupV1.Delete("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.delete"), h.DeleteLesson)
|
||||||
|
|
@ -178,15 +176,6 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Put("/questions/:id", a.authMiddleware, a.RequirePermission("questions.update"), h.UpdateQuestion)
|
groupV1.Put("/questions/:id", a.authMiddleware, a.RequirePermission("questions.update"), h.UpdateQuestion)
|
||||||
groupV1.Delete("/questions/:id", a.authMiddleware, a.RequirePermission("questions.delete"), h.DeleteQuestion)
|
groupV1.Delete("/questions/:id", a.authMiddleware, a.RequirePermission("questions.delete"), h.DeleteQuestion)
|
||||||
|
|
||||||
// FAQs
|
|
||||||
groupV1.Get("/faqs", h.ListPublicFAQs)
|
|
||||||
groupV1.Get("/faqs/:id", h.GetPublicFAQByID)
|
|
||||||
groupV1.Get("/admin/faqs", a.authMiddleware, a.RequirePermission("faqs.list"), h.ListFAQsAdmin)
|
|
||||||
groupV1.Get("/admin/faqs/:id", a.authMiddleware, a.RequirePermission("faqs.get"), h.GetFAQByIDAdmin)
|
|
||||||
groupV1.Post("/admin/faqs", a.authMiddleware, a.RequirePermission("faqs.create"), h.CreateFAQ)
|
|
||||||
groupV1.Put("/admin/faqs/:id", a.authMiddleware, a.RequirePermission("faqs.update"), h.UpdateFAQ)
|
|
||||||
groupV1.Delete("/admin/faqs/:id", a.authMiddleware, a.RequirePermission("faqs.delete"), h.DeleteFAQ)
|
|
||||||
|
|
||||||
// Question Sets
|
// Question Sets
|
||||||
groupV1.Post("/question-sets", a.authMiddleware, a.RequirePermission("question_sets.create"), h.CreateQuestionSet)
|
groupV1.Post("/question-sets", a.authMiddleware, a.RequirePermission("question_sets.create"), h.CreateQuestionSet)
|
||||||
groupV1.Get("/question-sets", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetQuestionSetsByType)
|
groupV1.Get("/question-sets", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetQuestionSetsByType)
|
||||||
|
|
@ -321,18 +310,6 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Delete("/notifications", a.authMiddleware, a.RequirePermission("notifications.delete_mine"), h.DeleteUserNotifications)
|
groupV1.Delete("/notifications", a.authMiddleware, a.RequirePermission("notifications.delete_mine"), h.DeleteUserNotifications)
|
||||||
groupV1.Get("/notifications/unread", a.authMiddleware, a.RequirePermission("notifications.count_unread"), h.CountUnreadNotifications)
|
groupV1.Get("/notifications/unread", a.authMiddleware, a.RequirePermission("notifications.count_unread"), h.CountUnreadNotifications)
|
||||||
groupV1.Post("/notifications/create", a.authMiddleware, a.RequirePermission("notifications.create"), h.CreateAndSendNotification)
|
groupV1.Post("/notifications/create", a.authMiddleware, a.RequirePermission("notifications.create"), h.CreateAndSendNotification)
|
||||||
groupV1.Post("/notifications/test-push", a.authMiddleware, a.RequirePermission("notifications.test_push"), h.SendTestPushNotification)
|
|
||||||
|
|
||||||
// Bulk Notifications
|
|
||||||
groupV1.Post("/notifications/bulk-push", a.authMiddleware, a.RequirePermission("notifications.bulk_push"), h.SendBulkPushNotification)
|
|
||||||
groupV1.Post("/notifications/bulk-sms", a.authMiddleware, a.RequirePermission("notifications.bulk_sms"), h.SendBulkSMS)
|
|
||||||
groupV1.Post("/notifications/send-email", a.authMiddleware, a.RequirePermission("notifications.send_email"), h.SendSingleEmail)
|
|
||||||
groupV1.Post("/notifications/bulk-email", a.authMiddleware, a.RequirePermission("notifications.bulk_email"), h.SendBulkEmail)
|
|
||||||
|
|
||||||
// Scheduled Notifications
|
|
||||||
groupV1.Get("/notifications/scheduled", a.authMiddleware, a.RequirePermission("notifications_scheduled.list"), h.ListScheduledNotifications)
|
|
||||||
groupV1.Get("/notifications/scheduled/:id", a.authMiddleware, a.RequirePermission("notifications_scheduled.get"), h.GetScheduledNotification)
|
|
||||||
groupV1.Post("/notifications/scheduled/:id/cancel", a.authMiddleware, a.RequirePermission("notifications_scheduled.cancel"), h.CancelScheduledNotification)
|
|
||||||
|
|
||||||
// Issues
|
// Issues
|
||||||
groupV1.Post("/issues", a.authMiddleware, a.RequirePermission("issues.create"), h.CreateIssue)
|
groupV1.Post("/issues", a.authMiddleware, a.RequirePermission("issues.create"), h.CreateIssue)
|
||||||
|
|
@ -347,6 +324,18 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Post("/devices/register", a.authMiddleware, a.RequirePermission("devices.register"), h.RegisterDeviceToken)
|
groupV1.Post("/devices/register", a.authMiddleware, a.RequirePermission("devices.register"), h.RegisterDeviceToken)
|
||||||
groupV1.Post("/devices/unregister", a.authMiddleware, a.RequirePermission("devices.unregister"), h.UnregisterDeviceToken)
|
groupV1.Post("/devices/unregister", a.authMiddleware, a.RequirePermission("devices.unregister"), h.UnregisterDeviceToken)
|
||||||
|
|
||||||
|
// Push Notifications
|
||||||
|
groupV1.Post("/notifications/test-push", a.authMiddleware, a.RequirePermission("notifications.test_push"), h.SendTestPushNotification)
|
||||||
|
groupV1.Post("/notifications/bulk-push", a.authMiddleware, a.RequirePermission("notifications.bulk_push"), h.SendBulkPushNotification)
|
||||||
|
groupV1.Post("/notifications/bulk-sms", a.authMiddleware, a.RequirePermission("notifications.bulk_sms"), h.SendBulkSMS)
|
||||||
|
groupV1.Post("/notifications/send-email", a.authMiddleware, a.RequirePermission("notifications.send_email"), h.SendSingleEmail)
|
||||||
|
groupV1.Post("/notifications/bulk-email", a.authMiddleware, a.RequirePermission("notifications.bulk_email"), h.SendBulkEmail)
|
||||||
|
|
||||||
|
// Scheduled Notifications
|
||||||
|
groupV1.Get("/notifications/scheduled", a.authMiddleware, a.RequirePermission("notifications_scheduled.list"), h.ListScheduledNotifications)
|
||||||
|
groupV1.Get("/notifications/scheduled/:id", a.authMiddleware, a.RequirePermission("notifications_scheduled.get"), h.GetScheduledNotification)
|
||||||
|
groupV1.Post("/notifications/scheduled/:id/cancel", a.authMiddleware, a.RequirePermission("notifications_scheduled.cancel"), h.CancelScheduledNotification)
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
groupV1.Get("/settings", a.authMiddleware, a.RequirePermission("settings.list"), h.GetGlobalSettingList)
|
groupV1.Get("/settings", a.authMiddleware, a.RequirePermission("settings.list"), h.GetGlobalSettingList)
|
||||||
groupV1.Get("/settings/:key", a.authMiddleware, a.RequirePermission("settings.get"), h.GetGlobalSettingByKey)
|
groupV1.Get("/settings/:key", a.authMiddleware, a.RequirePermission("settings.get"), h.GetGlobalSettingByKey)
|
||||||
|
|
|
||||||
|
|
@ -1,623 +0,0 @@
|
||||||
{
|
|
||||||
"info": {
|
|
||||||
"_postman_id": "8bbdb568-d64a-4f8e-8f89-0f57f6e7a65e",
|
|
||||||
"name": "FAQ Management - Complete Flow",
|
|
||||||
"description": "Complete collection for FAQ feature: public listing/detail and admin CRUD management with validation/error cases.",
|
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"type": "bearer",
|
|
||||||
"bearer": [
|
|
||||||
{
|
|
||||||
"key": "token",
|
|
||||||
"value": "{{access_token}}",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "base_url",
|
|
||||||
"value": "http://localhost:8080"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "access_token",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "faq_id",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "faq_id_inactive",
|
|
||||||
"value": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "01 - Admin FAQ CRUD",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create FAQ (ACTIVE)",
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"pm.test(\"Status code is 201\", function () {",
|
|
||||||
" pm.response.to.have.status(201);",
|
|
||||||
"});",
|
|
||||||
"const body = pm.response.json();",
|
|
||||||
"pm.test(\"FAQ ID exists\", function () {",
|
|
||||||
" pm.expect(body.data.id).to.be.a(\"number\");",
|
|
||||||
"});",
|
|
||||||
"pm.collectionVariables.set(\"faq_id\", body.data.id);"
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json",
|
|
||||||
"type": "text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"question\": \"How do I reset my password?\",\n \"answer\": \"Go to login and click 'Forgot Password'.\",\n \"category\": \"Account\",\n \"display_order\": 1,\n \"status\": \"ACTIVE\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create FAQ (INACTIVE)",
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"pm.test(\"Status code is 201\", function () {",
|
|
||||||
" pm.response.to.have.status(201);",
|
|
||||||
"});",
|
|
||||||
"const body = pm.response.json();",
|
|
||||||
"pm.collectionVariables.set(\"faq_id_inactive\", body.data.id);"
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json",
|
|
||||||
"type": "text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"question\": \"How can I change app language?\",\n \"answer\": \"Open settings and choose language preference.\",\n \"category\": \"General\",\n \"display_order\": 50,\n \"status\": \"INACTIVE\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List FAQs (Admin - All)",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs?limit=20&offset=0",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"query": [
|
|
||||||
{
|
|
||||||
"key": "limit",
|
|
||||||
"value": "20"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "offset",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List FAQs (Admin - Filter ACTIVE)",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs?status=ACTIVE&limit=20&offset=0",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"query": [
|
|
||||||
{
|
|
||||||
"key": "status",
|
|
||||||
"value": "ACTIVE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "limit",
|
|
||||||
"value": "20"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "offset",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get FAQ By ID (Admin)",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs/{{faq_id}}",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs",
|
|
||||||
"{{faq_id}}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update FAQ (Admin)",
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"pm.test(\"Status code is 200\", function () {",
|
|
||||||
" pm.response.to.have.status(200);",
|
|
||||||
"});"
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json",
|
|
||||||
"type": "text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"question\": \"How do I reset my account password?\",\n \"answer\": \"Tap 'Forgot Password' on login and follow OTP steps.\",\n \"category\": \"Account\",\n \"display_order\": 2,\n \"status\": \"ACTIVE\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs/{{faq_id}}",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs",
|
|
||||||
"{{faq_id}}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Archive FAQ (Set INACTIVE)",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json",
|
|
||||||
"type": "text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"status\": \"INACTIVE\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs/{{faq_id}}",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs",
|
|
||||||
"{{faq_id}}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "02 - Public FAQ Endpoints",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "List Public FAQs (Only ACTIVE)",
|
|
||||||
"request": {
|
|
||||||
"auth": {
|
|
||||||
"type": "noauth"
|
|
||||||
},
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/faqs?limit=20&offset=0",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"query": [
|
|
||||||
{
|
|
||||||
"key": "limit",
|
|
||||||
"value": "20"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "offset",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List Public FAQs by Category",
|
|
||||||
"request": {
|
|
||||||
"auth": {
|
|
||||||
"type": "noauth"
|
|
||||||
},
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/faqs?category=Account&limit=20&offset=0",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"faqs"
|
|
||||||
],
|
|
||||||
"query": [
|
|
||||||
{
|
|
||||||
"key": "category",
|
|
||||||
"value": "Account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "limit",
|
|
||||||
"value": "20"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "offset",
|
|
||||||
"value": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get Public FAQ By ID (ACTIVE only)",
|
|
||||||
"request": {
|
|
||||||
"auth": {
|
|
||||||
"type": "noauth"
|
|
||||||
},
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/faqs/{{faq_id}}",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"faqs",
|
|
||||||
"{{faq_id}}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get Public FAQ By ID (INACTIVE should be 404)",
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"pm.test(\"Status code is 404\", function () {",
|
|
||||||
" pm.response.to.have.status(404);",
|
|
||||||
"});"
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"request": {
|
|
||||||
"auth": {
|
|
||||||
"type": "noauth"
|
|
||||||
},
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/faqs/{{faq_id_inactive}}",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"faqs",
|
|
||||||
"{{faq_id_inactive}}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "03 - Validation & Auth Errors",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create FAQ Missing Question - Expect 400",
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"pm.test(\"Status code is 400\", function () {",
|
|
||||||
" pm.response.to.have.status(400);",
|
|
||||||
"});"
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json",
|
|
||||||
"type": "text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"answer\": \"Missing question should fail\",\n \"status\": \"ACTIVE\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create FAQ Invalid Status - Expect 400",
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"pm.test(\"Status code is 400\", function () {",
|
|
||||||
" pm.response.to.have.status(400);",
|
|
||||||
"});"
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json",
|
|
||||||
"type": "text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"question\": \"Sample question\",\n \"answer\": \"Sample answer\",\n \"status\": \"PUBLISHED\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List Admin FAQs Without Auth - Expect 401/403",
|
|
||||||
"request": {
|
|
||||||
"auth": {
|
|
||||||
"type": "noauth"
|
|
||||||
},
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get Missing FAQ (Admin) - Expect 404",
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"pm.test(\"Status code is 404\", function () {",
|
|
||||||
" pm.response.to.have.status(404);",
|
|
||||||
"});"
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs/99999999",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs",
|
|
||||||
"99999999"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "04 - Cleanup",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Delete FAQ (ACTIVE/UPDATED)",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs/{{faq_id}}",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs",
|
|
||||||
"{{faq_id}}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete FAQ (INACTIVE)",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/admin/faqs/{{faq_id_inactive}}",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"admin",
|
|
||||||
"faqs",
|
|
||||||
"{{faq_id_inactive}}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user