Content & Community (Posts, Blog, Trainings, Skills, Ratings, Feature Requests)
Part of the Anaya Care product wiki. See 00-overview.md.
Purpose
This module groups the "content" and "community" features of the platform:
- Posts — a per-business social feed (text, image/video attachments, hashtags, mentions) with likes, comments, threaded replies, and bookmarks, plus automated birthday celebration posts (
apps/backend/src/posts/). - Blog — a platform-global, publicly readable marketing/resources blog with AI-assisted bulk authoring (OpenAI content + Gemini cover images) (
apps/backend/src/blog/). - Trainings — a full LMS: training courses with sections, videos, AI-generated slides/videos/assessments, learner progress, assignments with due dates, certificates with public verification, and a platform catalog that businesses can fork ("avail") (
apps/backend/src/trainings/). - Skills — a flat global skill catalog referenced by care provider profiles and job postings (
apps/backend/src/skills/). - Ratings — star ratings (0–5) of care providers by other users, feeding an average-rating field on the care provider profile (
apps/backend/src/ratings/). - Feature Requests — business users submit product ideas; SuperAdmin triages them through a status pipeline (
apps/backend/src/feature-requests/).
Entities & Data Model
Posts module
| Entity | Collection | Source |
|---|---|---|
Post | posts | apps/backend/src/posts/entities/post.entity.ts |
Comment | comments | apps/backend/src/posts/entities/comment.entity.ts |
Like | likes | apps/backend/src/posts/entities/like.entity.ts |
Bookmark | bookmarks | apps/backend/src/posts/entities/bookmark.entity.ts |
Post key fields (post.entity.ts):
| Field | Type | Notes |
|---|---|---|
business | ObjectId → Business | required, indexed — feed is tenant-scoped |
content | string | required |
author | ObjectId → User | required |
attachments | {type: 'image'|'video', url, blurhash?, metadata?}[] | embedded |
hashtags | string[] | indexed |
mentions | ObjectId[] → User | indexed; triggers mention notifications |
likesCount / commentsCount / bookmarksCount | number | denormalized counters |
type | PostType (regular | birthday) | default regular |
celebrant | ObjectId → User | only on birthday posts |
isDeleted / deletedAt / deletedBy | soft delete |
Comment key fields: business, content, author, post, parentComment? (one nesting level), replies[], repliesCount (max 500, enforced in a pre-save hook and again in createComment — comment.entity.ts, posts.service.ts:840-846), likesCount, soft-delete fields.
Like: polymorphic via target + targetType (Post|Comment, Mongoose refPath). Unique index on {user, target, targetType}.
Bookmark: {user, post} unique index; posts only.
Blog module
| Entity | Collection | Source |
|---|---|---|
BlogPost | blog_posts | apps/backend/src/blog/entities/blog-post.entity.ts |
Key fields: title (≤200), slug (unique, lowercase), content (Markdown), excerpt (≤500), coverImage {url, alt?, blurhash?}, author → User, status (BlogPostStatus: draft/published/archived — packages/shared/src/enums/domain-status.ts:648), tags[], publishedAt, isFeatured, isDeleted, seoMeta {title?, description?}, coverImagePrompt, readingTimeMinutes (computed at ~200 wpm from markdown-stripped content — blog.service.ts:14-23), viewCount. No business field — blog is platform-global and the controller is @SkipBusinessScope() (blog.controller.ts:38).
Trainings module
| Entity | Collection | Source |
|---|---|---|
Training | trainings | apps/backend/src/trainings/entities/training.entity.ts |
TrainingProgress | training_progress | training-progress.entity.ts |
TrainingAssignment | training_assignments | training-assignment.entity.ts |
TrainingCertificate | training_certificates | training-certificate.entity.ts |
BusinessTraining | business_trainings | business-training.entity.ts (audit-only fork log) |
TrainingGenerationJob | training_generation_jobs | training-generation-job.entity.ts |
Training key fields: title, description, coverPhoto(+Blurhash, +ImagePrompt), photoBank[] (reference images for AI slide image generation, max 10 per a comment — not enforced in schema), sections[] (embedded; each with id (uuid), title, content, order, optional assessment {questions (multiple_choice/yes_no), passingScore, maxAttempts?, required}, contentType (none/video/slides), unified videoType/videoKey/videoBlurhash (S3 keys only; signed URLs generated on demand), slides[] with speechScript, video-generation tracking + captionKey), status (TrainingStatus), business (ObjectId or null = platform training), sourceTrainingId (provenance for catalog forks), isPublic, isDeleted, review fields (submittedForReviewAt, reviewedBy, reviewedAt, rejectionReason), createdBy, generation progress fields.
TrainingProgress: {training, user} unique; sectionProgress[] with per-section isCompleted, assessmentAttempts[] (responses, score 0–100, passed), best assessmentScore, assessmentPassed; overallProgress (0–100); startedAt/lastAccessedAt/completedAt.
TrainingAssignment: business (required), training, user, assignedBy, assignedAt, dueDate?, currentStatus (TrainingAssignmentStatus: ASSIGNED/IN_PROGRESS/COMPLETED/OVERDUE), append-only statusHistory[] (with isSystemAction), links to trainingProgress and certificate, soft delete. Unique {training, user} where isDeleted: false.
TrainingCertificate: certificateNumber unique, format CERT-YYYYMM-XXXXXX from a confusion-free charset (training-certificate.service.ts:33-66), pdfKey (S3), rich metadata (overall score, per-section assessment results, time spent), verificationCount/lastVerifiedAt for public verification.
BusinessTraining: audit row per catalog fork — {business, sourceTraining, clonedTraining, availedBy, availedAt}. Explicitly "no longer the source of truth for visibility" (entity header comment); no uniqueness, re-avail produces a fresh fork each time.
TrainingGenerationJob: KB-driven AI generation job — selectedDocuments[] → KnowledgeBaseDocument, promptOptions (audience/tone/objectives/focusAreas, 3–12 sections, 200–1500 words/section), status (TrainingGenerationStatus, 12 values incl. searching/generating/self_checking/refining — domain-status.ts:500), currentIteration/maxIterations (default 5), currentDraft, iterationLogs[], accumulatedContext[], autoGenerationOptions/Results (cover photo, slides, videos, assessments), requestedBy.
Skills, Ratings, Feature Requests
Skill (skills collection, apps/backend/src/skills/entities/skill.entity.ts): just name (unique) + description?. No business field — one global catalog shared by all tenants.
Rating (ratings collection, apps/backend/src/ratings/entities/rating.entity.ts): business (required, auto-injected by the global business-scope Mongoose plugin since the service never sets it — apps/backend/src/common/plugins/business-scope.plugin.ts), user (the rated care provider), rater, score (0–5), comment?, isVerified (default false), verifiedAt?, verifiedBy?.
FeatureRequest (feature_requests collection, apps/backend/src/feature-requests/entities/feature-request.entity.ts): business, submittedBy, title (≤200), description (≤5000), category (FeatureRequestCategory, 9 values — domain-status.ts:1059), status (FeatureRequestStatus: New / Under Review / Planned / In Progress / Completed / Declined — domain-status.ts:1047), platformNotes (≤5000, SuperAdmin-only).
Workflows & State Machines
Social feed posts
- Create/read: any authenticated user in a business hits
POST /posts/GET /posts(posts.controller.ts). The feed is hard-scoped to the caller's business and uses cursor pagination (_id < cursor, newest first) with$lookups for author, mentions, per-viewerisLiked/isBookmarked(posts.service.ts:69+). - Update/delete: only the author can update or soft-delete their own post — the query filters
author: userId(posts.service.ts:437-447, 468-477). There is no admin/moderator override path in code. - Reactions: single "like" toggle on posts and comments (no reaction types). Unique like per user/target; denormalized
likesCountvia$inc. Liking a post notifies the author (posts.service.ts:541-546→PostNotificationService.handlePostLike). - Comments: one level of nesting (
parentCommentId); replies incrementrepliesCounton the parent (cap 500) instead of the post'scommentsCount(posts.service.ts:860-885). Top-level comments notify the post author; replies notify the parent-comment author; post mentions notify mentioned users (posts.service.ts:57-65). - Bookmarks: per-user toggle +
GET /posts/bookmarkslist. - Birthday celebration posts (the "celebrations" feature): a cron at 9:00 AM America/Los_Angeles daily (
posts/tasks/birthday.tasks.ts,@Cron('0 9 * * *')) finds active users whose birthday is today (Feb 29 celebrated Feb 28 in non-leap years —birthday.service.ts:55-80), skips users with no business, dedupes one birthday post per celebrant per day, creates atype: birthdaypost authored by the celebrant with a random canned message and hashtags['birthday','celebration'](birthday.service.ts:createBirthdayPost), then sends aBirthdayTodayunified (in-app + push) notification to all active users in the same business with link/dashboard/posts/{postId}(birthday.tasks.ts:sendBirthdayNotificationToAll). - Read endpoints:
GET /posts/birthdays/today(today's birthday posts),/upcoming?days=,/range?pastDays=&futureDays=(default −15/+30, bucketed into recent/today/upcoming), and a diagnosticGET /posts/birthdays/debug/:username(posts.controller.ts:80-115). - The
showBirthdayInFeeduser privacy flag exists onUser(users/entities/user.entity.ts:207) and is toggleable in mobile (apps/mobile/components/ui/birthday-privacy-toggle.tsx), but all three birthday queries have the filter commented out with// TODO: Re-enable when UI toggle is added(birthday.service.ts:88, 137, 215) — the toggle currently has no effect.
There is no post status machine — posts are live immediately; no draft/approval/flagging flow exists in the backend (despite POSTS_FLAG/POSTS_MODERATE permissions existing, see Gaps).
Blog authoring & publishing
- Admin endpoints (
POST /blogs,GET /blogs/admin/list,GET /blogs/admin/:id,PATCH /blogs/:id,DELETE /blogs/:id,DELETE /blogs/all) requirePOSTS_MANAGEwith a real controller-levelPermissionsGuard(blog.controller.ts:48-...). Note blog reuses the posts permission; there is no dedicated blog permission. - Public endpoints (
@Public()):GET /blogs/posts(published only),/posts/featured(default 3),/posts/tags,/posts/:slug(incrementsviewCounton every fetch —blog.controller.ts:findBySlug). - AI bulk generation:
POST /blogs/bulk-generateenqueues a BullMQblog-generationjob; per topic it skips existing slugs, generates structured content via the OpenAI Responses API (modelgpt-4o) with a strict JSON schema (blog/services/blog-ai.service.ts:generateBlogContent), optionally generates a 16:9 cover photo via Gemini uploaded to S3 with blurhash (generateCoverImage), retries slug collisions with a timestamp suffix, optionally auto-publishes, and optionally marks 4 random created posts as featured (blog/processors/blog-generation.processor.ts). Job listing/status endpoints expose queue state (blog.controller.ts:97-160).POST /blogs/generate-covergenerates a single cover on demand (503 if Gemini unavailable). - Web editor: platform admins author posts at
apps/web/app/(app)/(platform)/platform/blog(list,new,[id]) using a Plate rich-text editor.apps/web/app/editor/page.tsxis an unauthenticated test page for the Plate editor (its own comment: "This test page uses components that require QueryClientProvider at runtime") — it is not part of the blog flow. - Public reading happens at
apps/web/app/(app)/(public)/blogs(/blogs,/blogs/[slug]) with SEO metadata/JSON-LD. (apps/web/app/(app)/(public)/featuresis a static marketing page, unrelated to feature requests.)
Trainings
Lifecycle state machine (TrainingStatus, domain-status.ts:478; transitions in trainings.service.ts:517-713):
- Ownership/visibility (
trainings.service.ts:findAll, isTrainingVisibleToUser, enforceTrainingOwnership): platform trainings havebusiness = nulland only SuperAdmin/System can modify them; business trainings are only modifiable by users of that business withTRAININGS_MANAGE. Non-managers see only published trainings of their business plus public+published platform trainings. A learner with existing progress keeps access to a training that was since unpublished (allowInProgressAccess). - Catalog ("avail") flow (
trainings/services/business-training.service.ts): the catalog is non-public, published platform trainings (getCatalogfilter{business: null, isPublic: false, status: published}).POST /trainings/business-trainings/:id/avail(permissionTRAININGS_CATALOG) deep-forks the training into a business-owned, immediately-publishedcopy with new section IDs but reused S3 keys (no asset duplication; hard-deleting the source breaks forks — entity comment intraining.entity.ts), plus aBusinessTrainingaudit row. Re-avail is intentionally unconditional and creates parallel forks. - Learner progress (
trainings.service.ts:1073-1338):POST /trainings/:id/progress/startrequires the training to bepublished; creates/touches aTrainingProgressdoc and emitstraining.started.POST /trainings/:id/progress/sections/:sectionIdrecords section completion and/or scores an assessment attempt server-side (percentage of exact-match answers vscorrectAnswer; sections without questions auto-score 100/pass). Required assessments gate completion;maxAttemptsblocks further failed attempts.overallProgress= completed sections / total sections; reaching 100% setscompletedAtand emitstraining.completed(and can regress below 100% if sections are added). - Completion side-effects (
trainings/listeners/training-completion.listener.ts): ontraining.completed→ generate certificate + mark any assignmentCOMPLETED; ontraining.started→ mark assignmentIN_PROGRESSand link the progress doc. - Certificates (
trainings/services/training-certificate.service.ts:generateCertificate): idempotent per{training, user}; validates 100% progress and all required assessments passed; generates a uniqueCERT-YYYYMM-XXXXXXnumber; renders a PDF via Puppeteer against the frontend certificate page (PdfService.generateTrainingCertificatePdf) and uploads to S3 (failure is non-fatal — PDF can be regenerated). Public, unauthenticated verification atGET /trainings/certificates/verify/:certificateNumberincrementsverificationCount(training-certificate.controller.ts). - Assignments (
trainings/services/training-assignment.service.ts): admins withTRAININGS_ASSIGNcreate single or bulk assignments of published trainings; if the user already has progress the initial status is back-filled toIN_PROGRESS/COMPLETED(createAssignment:96-104). A daily midnight cron bulk-marks past-dueASSIGNED/IN_PROGRESSassignmentsOVERDUEwith a system status-history entry (trainings/tasks/training-assignment.tasks.ts,markOverdueAssignments). No notification is sent on assignment or overdue (none found in the service).
- AI content pipeline (all behind
TRAININGS_MANAGE):POST /trainings/generateruns an agentic BullMQtraining-generationjob over selected knowledge-base documents: iterative search → generate → self-check → refine loop (max 5 iterations), accumulating search context and confidence, then creates the Training and optionally auto-generates cover photo, slides, videos, and assessments (training-generation-job.entity.ts,processors/training-generation.processor.ts).- Per-section slide CRUD/generation, slide-image regeneration (Gemini, with
photoBankimages as reference context), AI assessment generation, and aslides-video-mergeBullMQ job that renders slides + TTS speech scripts into a section video with captions (SlideMergeStatus: pending/processing/completed/failed) (trainings.controller.ts:300-540,processors/slides-video-merge.processor.ts).
Skills
No workflow. CRUD on the global catalog plus POST /skills/populate, which idempotently seeds 20 hardcoded caregiving skills (skills.service.ts:populateSkills). findOrCreateSkillsByNames (case-insensitive find-or-create) exists but has no callers anywhere in the backend — dead code. Skills attach by ObjectId reference to:
- Care provider profiles — top-level
skills[]and per-WorkExperienceskills[](apps/backend/src/users/entities/care-provider-profile.entity.ts:49,164), populated during mobile profile setup (apps/mobile/app/setup-profile/careprovider/skills.tsx,add-work-experience.tsx). - Job postings —
JobPosting.skills[](apps/backend/src/job-postings/entities/job-posting.entity.ts:94), selected in the web job-posting form and populated (id name) throughoutjob-postings.service.ts. No automated skill-matching algorithm between providers and postings was found — matching is human/visual.
Ratings
POST /ratings: the authenticated caller (rater) rates a target user (userId, called "care provider" in the service but any existing user passes — there is no role check on the target,ratings.service.ts:create). Score 0–5, optional comment. After save,CareProviderService.recalculateAverageRating(userId)runs fire-and-forget (failures logged, non-blocking).GET /ratings?userId=&isVerified=&sortOrder=lists with pagination and population; sort by recency or score.- Verification:
POST /ratings/:id/verifysetsisVerified/verifiedAt/verifiedBy;DELETE /ratings/:id/rejectsimply deletes the rating (same handler asremove). There is no role/permission restriction on any ratings endpoint beyond JWT auth (ratings.controller.tsuses onlyAuthGuard('jwt')). - No state machine beyond
unverified → verified(or deletion). No duplicate-rating prevention (one user can rate the same person repeatedly).
Feature requests
- Business side (
REQUESTS_MANAGEpermission — shared with the operational Requests module, see 13-requests.md):POST /feature-requestscreates;GET /feature-requestslists own business's requests withstatus,platformNotes,business,submittedBystripped — submitters deliberately cannot see triage state (feature-requests.service.ts:findAllForBusiness). - Platform side (SuperAdmin/System via
PLATFORM_ROLES—user-role.ts:90):GET /feature-requests/platform(with per-status tab counts scoped to category/search filters),GET/PATCH /feature-requests/platform/:idto setstatus/platformNotes. - No voting, no comments, no de-duplication — it is a plain submit-and-triage pipeline.
Business Rules & Constraints
- Posts permission decorators are not enforced.
PostsControllerapplies onlyJwtAuthGuard(posts.controller.ts:36); its@RequirePermissions(...)decorators rely on the globally registeredPermissionsGuard(app.module.ts:292-294), which intentionally defers (returns true) whenreq.useris not yet populated — and global guards run before the controller-levelJwtAuthGuard(auth/guards/permissions.guard.ts:64-72). Unlike blog/trainings/feature-requests controllers, posts never re-appliesPermissionsGuardat controller level, so every authenticated user can create posts, like, comment, and bookmark regardless ofPOSTS_MANAGE. This is also why CareProviders (whose defaults grant onlyPOSTS_VIEW_ASSIGNED+POSTS_FLAG—packages/shared/src/constants/default-role-permissions.ts:238-239) can interact with the feed from mobile. - Posts are tenant-isolated by explicit
businessmatch in every aggregation (posts.service.ts:75-78) plus the global business-scope Mongoose plugin (common/plugins/business-scope.plugin.ts). - Only the post/comment author may edit or delete their content (
posts.service.ts:437, 468, 1092+, 1124+); no moderator override exists. - Max 500 replies per comment — double-enforced (
comment.entity.tspre-save;posts.service.ts:840-846). One like per user per target (unique index). One bookmark per user per post (unique index). - Birthday posts: one per celebrant per calendar day (
birthday.service.ts:checkIfBirthdayPostExists); skipped for users without a business;showBirthdayInFeedis currently ignored (commented out). - Blog slugs are globally unique; conflict → 409 (
blog.service.ts:create/update).publishedAtis set only on the first transition topublished. Deletes are soft (isDeleted), including the bulkDELETE /blogs/all. - Blog admin endpoints reuse
POSTS_MANAGE; any business Admin holding it can manage the platform-global blog (blog has no business scoping) (blog.controller.ts). - Trainings: business-authored trainings must pass SuperAdmin review (
pending_review) before publishing; platform trainings publish directly (trainings.service.ts:publish:570-607). Onlydraft/rejectedcan be (re)submitted; onlypending_reviewcan be approved/rejected;unpublishallowed frompublished/pending_review;voidhas no status precondition. - Only published trainings can be started (
startTraining:1102-1104) or assigned (createAssignment:66-68). - Catalog avail requires: platform training (
business === null), not public, published (business-training.service.ts:54-68). Forks are published immediately and never receive upstream updates (permanent snapshot). - Assessment scoring is server-side; required assessments block section completion;
maxAttemptsis only enforced when the new attempt fails (trainings.service.ts:1245-1253). - One certificate per
{training, user}(idempotent); requires 100% progress + all required assessments passed; certificate verification endpoint is public/unauthenticated (training-certificate.controller.ts:verifyCertificate). - Assignments: unique per
{training, user}among non-deleted (partial index); overdue flip is system-only via the midnight cron. - Skills/Ratings: no permission checks at all beyond JWT — any authenticated user of any role can create/update/delete global skills (including cross-tenant impact, since the catalog is shared) and create/verify/delete ratings (
skills.controller.ts,ratings.controller.ts). - Rating
businessis auto-set from CLS by the business-scope plugin, and reads are auto-scoped to the caller's business; SuperAdmin (null business context) sees all (business-scope.plugin.ts). - Feature requests: business users never see
status/platformNotes(field-stripped infindAllForBusiness); onlyPLATFORM_ROLES(SuperAdmin, System) can read cross-tenant or update; search input is regex-escaped (escapeRegExpfromcare-proposals/utils/security.util).
Surfaces (Web & Mobile)
Web (apps/web)
| Surface | Route | Who | What |
|---|---|---|---|
| Feed | app/(app)/(admin)/dashboard/posts (+[id]) | Admin dashboard users | Business feed: list, detail, create/comment/like (uses /posts API) |
| Celebrations | app/(app)/(admin)/dashboard/celebrations | Admin dashboard users | Tabs of today/upcoming/recent birthdays from GET /posts/birthdays/range; "send greeting" dialog posts to POST /posts/birthdays/:userId/greetings — an endpoint that does not exist in the backend (celebrations/page.tsx:194, 214; no such route in posts.controller.ts) |
| Blog admin | app/(app)/(platform)/platform/blog (+new, [id]) | SuperAdmin | Authoring with Plate editor, bulk AI generation, publish/feature |
| Blog public | app/(app)/(public)/blogs (+[slug]) | Anonymous | Published posts, tags, featured, SEO/JSON-LD |
| Editor test page | app/editor/page.tsx | Anyone (no auth wrapper) | Standalone Plate editor playground; self-described test page |
| Trainings (business) | app/(app)/(admin)/dashboard/trainings (+add, [id]) | Admin (TRAININGS_MANAGE) | Author/edit sections, slides, assessments, publish→review |
| Training catalog | app/(app)/(admin)/dashboard/training-catalog | Admin (TRAININGS_CATALOG) | Browse platform catalog, avail (fork), see availed list |
| Training center | app/(app)/(admin)/dashboard/training-center | — | Redirect-only stub to /dashboard/trainings (training-center/page.tsx) |
| Trainings (platform) | app/(app)/(platform)/platform/trainings (+add, [id]) | SuperAdmin | Author platform/catalog trainings; approve/reject business submissions |
| Skills | app/(app)/(platform)/platform/skills | SuperAdmin UI (API unrestricted) | Global skill catalog CRUD |
| Ratings | components/user-reviews/* used in dashboard/clients/[id]/edit/care-team/_components/caregiver-card.tsx and components/user-profile/* | Admin dashboard users | Create/edit/list reviews of care providers; verify |
| Feature requests (business) | app/(app)/(admin)/dashboard/feature-requests | Admin (REQUESTS_MANAGE) | Submit + list own (no status visible) |
| Feature requests (platform) | app/(app)/(platform)/platform/feature-requests | SuperAdmin | Triage: status tabs, update status/notes |
Mobile (apps/mobile)
| Surface | Route | What |
|---|---|---|
| Feed | app/(app)/(feed)/index.tsx, bookmarks.tsx, [id].tsx + components/post/create-post-bottom-sheet.tsx | Full feed for care providers/families: create, like, comment, reply, bookmark (lib/posts-api.ts covers all /posts endpoints) |
| Post deep link | app/(post)/[id].tsx | Standalone post detail screen (notification deep-link target) |
| Trainings | app/(trainings)/index.tsx, [id]/index.tsx, [id]/[sectionId].tsx | Learner view: published trainings list (status=published), my progress, my certificates; section player + assessments |
| Celebrations | app/(celebrations)/_layout.tsx | Layout only — the index.tsx screen its own doc comment describes does not exist. The route group is effectively dead; only the birthday privacy toggle (components/ui/birthday-privacy-toggle.tsx) ships, and the backend ignores that flag anyway |
| Skills | app/setup-profile/careprovider/skills.tsx, add-work-experience.tsx (lib/skills-api.ts) | Care provider onboarding picks skills from the global catalog |
Frontend-only rules of note: the mobile trainings list filters to status=published client-side via query param; web celebrations greeting flow is broken at the API layer (above); no mobile UI exists for ratings or feature requests.
Cross-Module Dependencies
- Posts → Notifications:
PostNotificationService(inapps/backend/src/notification/post-notification.service.ts) sendsPostLiked,PostCommented,CommentLiked,CommentReplied,PostMentioned(packages/shared/src/enums/notification-types.ts:30-34); birthday cron sendsBirthdayTodayviaNotificationService.sendUnifiedNotification(posts/tasks/birthday.tasks.ts). See 12-communication.md. - Posts ← Users: birthday detection reads
User.birthday/isActive/business(posts/birthday.service.ts). - Trainings → Knowledge Base: AI generation searches selected
KnowledgeBaseDocuments (trainings.service.tsctor injectsKnowledgeBaseService;training-generation-job.entity.ts). - Trainings → Files/PDF: signed URLs for section videos/captions, certificate PDFs via
PdfService(Puppeteer) +FilesServiceS3 upload (training-certificate.service.ts). - Trainings → AI services:
TrainingAIContentService,SlidesVideoService, Gemini image generation; BullMQ queuestraining-generationandslides-video-merge(trainings.module.ts,processors/). - Blog → AI/Files: OpenAI (gpt-4o) +
GeminiService+FilesService(blog/services/blog-ai.service.ts); BullMQ queueblog-generation. - Skills → Job Postings:
JobPosting.skillsrefs (job-postings/entities/job-posting.entity.ts:94); see 06-job-postings.md. - Skills → Users:
CareProviderProfile.skillsand work-experience skills (users/entities/care-provider-profile.entity.ts:49,164); also referenced in shift generation UI (apps/web/.../shifts/_components/shift-generation-form.tsx). - Ratings → Users:
CareProviderService.recalculateAverageRatingupdates the care provider profile's average (ratings.service.ts); displayed on user profiles/care-team cards. Ratings are not linked to shifts/visits — they are free-floating user-to-user ratings. - Feature Requests ↔ Requests module: gated by the same
REQUESTS_MANAGEpermission as operational requests (13-requests.md) and reusesescapeRegExpfrom care-proposals utils. - Permissions: all module permissions live in
packages/shared/src/enums/business-permission.tswith role defaults inpackages/shared/src/constants/default-role-permissions.ts;Owner/SuperAdmin/Systembypass all checks (user-role.ts:254).
Open Questions & Gaps
- Posts permissions are decorative.
@RequirePermissionson everyPostsControllerroute is never evaluated because the controller lacks a controller-levelPermissionsGuardand the global instance defers pre-auth (posts.controller.ts:36;permissions.guard.ts:64-72). Either this is intentional ("everyone in the business may use the feed") or a missing guard — cannot determine from code. IfPOSTS_MANAGEwas meant to gate creation, it is currently bypassed. - Web celebrations "send greeting" calls a nonexistent endpoint.
dashboard/celebrations/page.tsx:214POSTs to/posts/birthdays/:userId/greetings; no such route exists inposts.controller.ts. Every greeting submission should 404. Broken feature. - Mobile celebrations route group is an empty shell —
app/(celebrations)/contains only_layout.tsx, whose comment documents anindex.tsxthat does not exist. Dead/dormant surface. showBirthdayInFeedprivacy toggle is a no-op. Mobile lets users toggle it, but all birthday queries have the filter commented out with a TODO (birthday.service.ts:88, 137, 215). Users who opt out still get public birthday posts and business-wide notifications — a privacy gap.- Skills and Ratings endpoints have no authorization beyond JWT: any authenticated user (e.g. a Family member) can delete global skills shared by all tenants, create ratings about anyone, verify ratings, or delete others' ratings (
skills.controller.ts,ratings.controller.ts). Also no duplicate-rating prevention and no role check that the rated user is actually a care provider. POSTS_FLAG,POSTS_MODERATE,POSTS_CONFIGURE,POSTS_VIEW_ALLare defined and granted in role defaults but referenced nowhere in the backend — no flagging/moderation endpoints exist. Planned-but-unbuilt moderation.- Blog admin reuses
POSTS_MANAGEeven though the blog is platform-global: a business Admin with the default permission set can, per the guard logic, create/edit/delete the public marketing blog (blog.controller.ts). Whether non-SuperAdmins are blocked elsewhere cannot be determined from code (web only exposes the UI under(platform)). DELETE /blogs/allsoft-deletes the entire blog in one authenticated call — dangerous surface for aPOSTS_MANAGEholder.- Birthday timezone: cron runs at 9 AM Pacific and "today" is computed from server-local
new Date()(birthday.service.ts:findUsersWithBirthdayToday), despite a comment claiming "uses the user's timezone". Users in other timezones may be celebrated on the wrong local day. - Posts debug endpoint in production:
GET /posts/birthdays/debug/:usernameexposes another user's birthday, activity status, and privacy flag to any feed-viewing user (posts.controller.ts:108-115). - Training
voidstatus is terminal and unguarded — any status (includinggenerating) can be voided, and no endpoint restores a voided training (trainings.service.ts:694). Intent unclear. - Catalog fork asset coupling: forks reuse S3 keys; hard-deleting a source training (the code only soft-deletes via API, but ops-level deletes would) breaks all forks — acknowledged in entity comments (
training.entity.tssourceTrainingId comment) but unmitigated. - Feature requests have no notification loop: submitters cannot see status by design and are never notified of outcomes; no events/notifications are emitted by
feature-requests.service.ts. Whether this is intended fire-and-forget feedback cannot be determined from code. findOrCreateSkillsByNamesis dead code (skills.service.ts) — no callers; likely a leftover from an AI flow.training-centerweb route is a redirect stub to/dashboard/trainings(training-center/page.tsx) — vestigial.- Module activity: trainings (last commit 2026-05-06), posts/blog (2026-04-24), feature-requests (2026-04-14) are active; ratings (2026-03-29) and especially skills (2026-01-23) are near-dormant. The
/editortest page and mobile(celebrations)group look abandoned. - Ratings
isVerifiedquery filter is typed@IsBoolean()on a query string (filter-rating.dto.ts); it filters only when truthy, andisVerified=falsecannot be used to list unverified-only ratings.