An end-to-end, full-fledged Learning Management System for instructor-led, funded training — run as a franchise.
Step-by-step, screenshot-driven guides for running the LMS from the admin panel — see docs/admin-guide/:
- Admin guide index — logging in, managing the store
- Changing a course fee
- Changing the currency conversion rate
Country catalogue maintenance: Malaysia and Ghana Course Info updates.
Tertiary Courses LMS is a complete, production-grade course-registration and learning-management platform built on OpenMage 1.x (Magento 1 LTS) and customised for Tertiary Infotech Academy. Every product is a course (instructor-led trainings, workshops, certifications) — there is no physical inventory or shipping. The storefront is the course-registration portal, and the admin panel is rebranded as a Training Management System for instructors and operations staff.
This system runs as a franchise model:
- 📚 Courseware is supplied and supported by Tertiary Courses Singapore. Franchisees plug into a ready-made catalogue of WSQ / IBF / SkillsFuture-aligned courses — they don't have to author content from scratch.
- 🛠️ Each franchise partner hosts their own server. The same codebase (this repo) is deployed to every partner's own server + database — one store per website, fully independent (own domain, currency, language, pricing, funding hooks). No shared multi-store install, no shared catalogue, no cross-site redirects.
- 🌏 Live partner sites: 🇸🇬
tertiarycourses.com.sg· 🇲🇾tertiarycourses.com.my· 🇬🇭tertiarycourses.com.gh.
Want to run an AI-powered future-tech training academy in your country — with a ready-made WSQ / IBF / SkillsFuture-aligned catalogue and this LMS set up on your own server? 👉 Apply to become a franchise partner — fill in the application form on the website and our team will be in touch.
| Feature | Description |
|---|---|
| 🎓 Course = Product | Catalogue of instructor-led / live-online / hybrid courses — no stock, weight, or shipping. |
| 🌐 Franchise model | Same codebase deployed to each partner's own server — one store per website (SG, MY, GH), each fully independent (own domain, currency, language, pricing). |
| 💰 Funding & subsidy hooks | SG SkillsFuture Credit / WSQ / IBF, MY HRDC — funding tiers (Baseline, MCES) auto-calculated. On WSQ courses the funding card is checkbox-driven: six Edit Course checkboxes (WSQ/CASL, MCES/SME, SFEC, UTAP, PSEA, Absentee Payroll) write the funding badge tags, and the card renders canonical per-scheme copy with SkillsFuture links generated from the course's own code. Every WSQ course page shows its SSG Funding Validity window (seeded from the TPG master list, editable per course in the admin General tab). The funding card's scheme links are proper buttons (SFC / SFEC / UTAP / PSEA), and learners submit their filled PSEA withdrawal form via a dedicated /psea-submission/ page that emails the attached form straight to admissions (no DB record). Beyond the government schemes, courses can also carry a Free AI Subscription perk badge — one tag drives both the storefront pill under the course title and a promotional "6 Months Free AI Subscription" card rendered directly above the WSQ Funding card, so the two can never drift apart. SEO guide pages walk learners through each journey — How to Register a WSQ Course, How to Register an IBF Course (both linked from the Enquiries mega-menu), How to Claim SkillsFuture Credit and How to Download SWDA SOA — each with step-by-step cards, FAQs and HowTo/FAQPage structured data. |
| 📋 Course-page section cards | Six per-course CMS block sections — Learning Outcomes, Brochure, Skills Framework, Certification, About IBF Certification, Funding — render as styled cards on every course page (block-first, with regex fallback from the description) and are edited per course in the Course Details tab. Partner-accreditation (ATC) cards — Microsoft Learning Partner, CompTIA, Linux Foundation, Autodesk ATC, Pearson VUE, Kryterion, PSI — render in the left column after the Certification card, followed by the Certification Exam venue card, driven by multi-select Edit Course checkboxes (atc_partners attribute) with canonical per-partner copy. |
| 🧾 Pro Forma Invoices | On-demand, self-sponsored SkillsFuture-claim pro formas with GST settled on the pre-subsidy list price. |
| 🏫 Automatic class formation | Orders materialise into classes & rosters out-of-band via cron — the storefront HTTP path stays untouched. |
| 📅 Class calendars | Role-aware calendar (month / day / year) of confirmed classes: trainers see My Calendar with their own assigned classes, learners see the classes they're enrolled in, and admins get a Training Calendar across all trainers. |
| 🎥 Virtual Training Link + Google Calendar sync | Each class carries its own Zoom / Google Meet link (course_runs.virtual_link, set on Create New Class or via the Agent API), shown to its trainer and learners on the Virtual Training Link card. A 15-minute cron links every upcoming class to its event on the site's Google Calendar, creating the event when missing (trainer invited, Google Meet link generated for an online class with no link). Runs per site through the domain-wide-delegated service account; inert on a site without the key. |
| 🧰 Classroom & AI tools | Sidebar tool launcher on the LMS dashboard — 18 groups (Ed Tools, Project Mgt, Problem Solving, Cyber Security, Finance, HR, Data Analytics, ML, Statistical, DOE, SPC, Sustainability, Networking, Blockchain, Quantum, GenAI, Agentic AI, Virtual) — identical for trainers and learners. |
| 📝 Course feedback → Reviews | QR-linked per-class feedback form (customisable via the Course Reviews → Form Builder) whose submissions save straight into the Magento review system as approved course reviews with per-question star ratings (Learning Outcome / Trainer Quality / Training Environment), browsable per course in the editor's Course Review tab with expandable full messages. Trainers show the QR from the Course Feedback section on their class page — a zoom icon (or clicking the QR) opens it full-window for projecting, centred, and Ctrl/Cmd + grows it to fill the screen; the form auto-fills the course title, code, run ID, assigned trainer and class start/end dates from the scanned class. |
| ⭐ Featured testimonials | Admins hand-pick marketing-worthy course reviews from the All Reviews grid (Featured column, filter, and bulk "Mark as Featured"), then manage the curated set on Course Reviews → Featured Reviews — a dedicated page showing exactly what the public sees, with live/hidden KPIs and one-click removal. The homepage shows 4 random featured reviews in a band directly under the blog strip, and /testimonials lists them all — each card showing the star rating, the quote and a link to the course the learner took. |
| 🔗 Social share & likes | Every course page carries a share bar (LinkedIn, X, Facebook, WhatsApp, Email, copy-link) and a thumbs-up Like button beside the pill-styled review summary — the same share row as the blog. The number on the button is the course page's view count (from Magento's report_event log, with an "N view(s)" tooltip); clicking still records a one-per-visitor like. |
| 👥 Six-role admin | Learner / Trainer / Developer / Marketing / Admin / Training Provider with session-based role switching and per-role page enforcement — each role reaches only its own pages (learners are allow-listed; trainer/marketing/developer are confined by a controller map; admin & Training Provider see everything). |
| 🎒 LMS login | Dedicated /lmslogin page on every site for the LMS roles (learner, trainer, developer, marketing) — email + password or email OTP; multi-role users pick a role, single-role users land straight on their dashboard. /learnerlogin remains as a legacy alias; admin & Training Provider sign in at the staff portal, which accepts every role. Two-column layout: the left column is self-registration (Name, Email, admin-issued Course Run ID, Role: Learner/Trainer, Learner by default) — a learner is enrolled on that class's roster and emailed a welcome asking them to log in (OTP) and download the latest course material; a trainer account is created inactive, pending admin approval, and the admin is notified. On desktop the page fits the window height (no scrolling); on mobile it stacks to a compact header, sign-in, then registration. "Powered by Tertiary Infotech Academy Pte Ltd" sits at the bottom of the sign-in column. |
| 🔁 SG → partner sync | One-way export of non-WSQ (C-prefix) courses to MY/GH — bulk "Sync All" or per-course "Sync One" from the partner admin, or triggered remotely from SG's Franchise Management (Course / Category / Schedule Sync via each partner's api_sync_trigger). Partner-owned course fees, schedules and trainer info are never overwritten on a course update; Schedule Sync explicitly replaces partner Course Date/Time options with SG's. |
| 🧲 Recommended Courses rail | Every SG course page shows at least 5 "Recommended Courses" (Upsell links); shortfalls are topped up with related WSQ courses by category affinity via scripts/maintenance/backfill-course-upsells.php (idempotent, no-op on partner sites). |
| 🌏 Franchise Management | Super Admin sidebar group with four pages: Franchise Report (confirmed + completed classes pulled from the MY/GH partner sites — class code, course, dates, trainer, attendance-marked learners — with filters, Sunday 10am auto-pull + Pull Now) and Course / Category / Schedule Sync (one-way SG → franchisee push triggers with per-partner selection; partners can never write back to SG). |
| 🛡️ Deploy safety guards | apply.php gives every migration an explicit @mms_instance identity, enforces the one-store-per-site topology invariant (a corrupting migration fails the deploy), drains the result of any row-returning statement so a guard's 'SELECT 1' no-op can't abort the chain on partner sites, and a daily 2AM maintenance cron publishes /media/health.json. |
| 🎟️ Payments | Stripe, HitPay, PayNow and bank transfer. |
| 📜 Certificates & attendance | Per-session (AM/PM) e-attendance per class day, plus certificate-of-achievement generation — auto-sent at 6:30pm when attendance exceeds 70%, with a trainer-managed per-learner delivery roster for manual sends. |
| 📣 Autonomous newsletter | SG-only agentic-flyer pipeline: designs a course flyer Mon & Thu 10am → manager approval (either manager, via email or admin) → MailerLite blast Mon & Thu 8am with a static R2-hosted registration QR. Hard cap 2 flyers/week for the autonomous flow; nothing sends without approval. The admin queue carries a per-row design brief — free-text special instructions (steers the AI copy and the colour scheme: named schemes or any hex repaint the whole design) plus a pinned intake date validated against the course's real published Course Dates; the brief dialog opens automatically the moment a course is added to the queue, so instructions are set before the design run can pick it up. A brief is never silently dropped: if the AI copy step fails, the run aborts instead of shipping the previous design's copy, and the queue row (the only copy of the brief) is restored to the head of the queue for a retry. A colour ask is persisted independently of the copy step, so it repaints even if the wording regeneration fails. |
| 📧 Newsletter subscriber sync | Daily 4am cron adds new order emails to the site's own MailerLite subscriber group, plus a one-shot historical backfill. Learners who previously unsubscribed are never re-added. API key, group, source store and on/off are all Company Settings, so every franchise site points at its own list. |
| ✍️ Agentic lead-magnet blog | /blog with slug URLs, SEO meta + BlogPosting / FAQPage / BreadcrumbList JSON-LD (FAQ schema auto-extracted from each post's native <details> accordions), Magento-tag reuse, likes, social share, R2 hero images. An agent team writes 2 posts/week: a research agent (Claude web search) scouts the latest AI topics, a writer agent produces an in-depth post with a branded, topic-themed auto-generated hero (13 keyword-picked themes incl. a Microsoft Copilot family, so a series of posts renders as one set), managers approve via email or the admin timeline, and approved posts publish Tue & Fri 9am, then auto-share to LinkedIn + Facebook. Admins queue the next courses from the Blog pipeline panel. |
| 🔌 Course catalog APIs | Key-authenticated read-only JSON feeds of the SG catalog — /courses/api_wsq (TGS- courses) and /courses/api_nonwsq (C- courses), kept as separate endpoints so the two funding tracks never mix. Add ?fields=full for the long description, suitability, prerequisites and assessment. Per-course and schedule lookups live at /courses/api_courses?sku= and /courses/api_schedule. Every endpoint is documented in the admin API Summary panel. |
| 🔍 AI-Overview-ready schema | Course pages emit rich Course JSON-LD — SKU as courseCode, aggregate review rating, and real upcoming class dates as CourseInstance entries parsed from the Course Date options — plus BreadcrumbList; the blog emits BlogPosting / FAQPage / CollectionPage. Structured data only, zero visual change. |
| 🎨 Ultimo storefront | Premium responsive theme + a custom dark admin theme. |
Scheduled, unsent newsletter flyers can be reopened with Request changes & review again.
The pipeline verifies MailerLite cancellation before regenerating from feedback,
releases the cancelled send slot, and requires fresh manager approval. Already
sending or blasted flyers cannot be reopened; existing social posts are unchanged.
Run the isolated regression checks with
docker exec ai-mms-web-1 php /var/www/html/scripts/local-dev/test-flyer-scheduled-changes.php.
Credentials → AI Provider selects Claude (existing OAuth/API configuration) or
OpenAI OAuth globally for email reply drafts, blog research/writing, newsletter
chat, flyer revisions, SEO and brochure copy. OpenAI uses gpt-5.6-sol via the
official @openai/codex-sdk (0.149.0). Optional, explicitly enabled fallback uses
@anthropic-ai/claude-agent-sdk (0.3.268) and a tested
Claude OAuth connection with claude-opus-5. OpenAI is tried first; failed
generation retries once through Claude with the same prompt/images/research mode.
Fallback use is logged in var/log/ai-provider.log; both failures hold the content.
The Claude OAuth — Opus 5 backup card tests and encrypts replacement tokens
without changing the primary provider. It also supplies the managed Claude
connection when Claude is selected as primary. Import a Codex
ChatGPT OAuth auth.json; Test & save provider validates model access before
activation. Only admin/training-provider roles can change the global connection.
OAuth tokens are encrypted in the database, never displayed, and materialised
only in private temporary sessions (no shell, plugins or app tools). Reference
images and blog research web search are preserved. Keep auth.json out of Git,
email and chat. Changing providers does not approve or send pending content.
See OpenAI authentication for device login.
Both OAuth-compatible SDKs run through scripts/ai/bridge.mjs with pinned,
lockfile-verified dependencies and Node 22 in Docker. The Codex SDK uses the
checksum-pinned Codex runtime; Claude Agent SDK uses its matching bundled runtime.
These are the official agent SDKs, not REST API-key clients. SDK environments
are isolated, tool access is disabled except explicitly requested research search,
and credentials/prompts are not included in error output. Run SDK boundary tests
with node --test scripts/ai/bridge.test.mjs after npm ci --prefix scripts/ai.
Claude-mode flyer generation routes subscription OAuth through the installed
Claude client; API keys use the Messages API. Organisation-level access denials hold the revision
without emailing rejected copy and display an actionable admin error. Authentication
routing checks: docker exec ai-mms-web-1 php /var/www/html/scripts/local-dev/test-flyer-claude-auth.php.
Provider checks: docker exec ai-mms-web-1 php /var/www/html/scripts/local-dev/test-ai-provider.php.
| Layer | Technology |
|---|---|
| Platform | OpenMage LTS v20.12.0 (Magento 1.x) |
| Language | PHP 8.2 |
| Database | MySQL 5.7 |
| Web Server | Apache 2.4 |
| Frontend Theme | Infortis Ultimo (responsive) |
| Caching | Redis (config / full-page / block) |
| Payments | Stripe, HitPay, PayNow, Bank Transfer |
| Aschroder SMTP Pro | |
| Containerisation | Docker Compose |
| Deployment | Coolify (auto-deploy on push to main) + Cloudflare R2 (media) |
FRANCHISE PARTNERS — same codebase, each on its own server + DB
🇸🇬 com.sg 🇲🇾 com.my 🇬🇭 com.gh
(one store per site · independent · no cross-site redirects)
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STOREFRONT (Ultimo theme · course-registration portal) │
│ Browse courses → register (cart/checkout) → pay → confirmation email │
└──────────────────────────────────────────────────────────────────────────┘
│ order = registration
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ BACKEND (cron, every 1 min — out-of-band, frontend untouched) │
│ ClassFormation → CourseRunEnrolmentService │
│ │ │ │
│ ▼ ▼ │
│ course_runs (class instance, course_run_enrolments (roster, │
│ class_id = C000042 …) idempotent INSERT IGNORE) │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ ADMIN (Training Management System · dark theme · six-role ACL) │
│ Classes · Rosters · Trainers · Attendance · Certificates · Pro Formas │
└──────────────────────────────────────────────────────────────────────────┘
Franchise data model — six axioms: Product = Course · Class identity = (course_code, title, start_date) · Class storage = one course_runs row (class_id like C000042 — uniform per-site sequence) · Order = Registration · Roster = course_run_enrolments · Users = six-role union with unified accounts.
ai-mms/
├── app/
│ ├── code/
│ │ ├── core/Mage/ # OpenMage LTS core
│ │ ├── local/MMD/ # Custom franchise modules (see below)
│ │ └── community/ # Stripe, HitPay
│ ├── design/ # Storefront + admin templates (.phtml)
│ └── etc/ # local.xml, modules/
├── skin/ # Ultimo theme + dark admin theme (CSS/JS)
├── lib/ # Varien / Zend / Magento libraries
├── migrations/ # Numbered *.sql applied by apply.php on deploy
├── scripts/ # local-dev fixups + maintenance scripts
├── docker/ # entrypoint.sh, php.ini, Apache config
├── docker-compose.yml # Local dev stack (web + MySQL)
└── Dockerfile # Production image
| Module | Purpose |
|---|---|
| RoleManager | Six-role admin system + class/roster management + class-id generation + role-aware class calendars (trainer/learner My Calendar, admin Training Calendar) + Google Calendar class-event sync (Model/Cron/GoogleCalendarSync.php). |
| Proforma | On-demand Pro Forma Invoice PDF (self-sponsored SFC claims; WSQ funding breakdown). |
| CourseImage | AI cover-image renderer + funding-badge tags (the same tags drive the storefront chips, the cover, the WSQ Funding card, and the Free AI Subscription perk card). |
| EmailLogin | Email-only admin login. |
| FlatCategoryUrl | Flat category URLs (/<url_key>.html) across all stores. |
| CustomOptions | Enhanced product options with SKU upgrade policies; class-schedule templates (Manage Class Schedule) plus a read-only Schedule Distribution report comparing active courses vs schedules per duration series, with rebalancing advice. |
| Enhancedsalesgrid | Admin sales-grid filters & rendering. |
| BankPayment | Bank-transfer payment method. |
| Branchscope | Legacy per-country store-view switcher — superseded by the one-store-per-site franchise model; retained for backward compatibility. |
| Certificate / Attendance | Certificates of achievement + e-attendance. |
| AccountSync | Unified learner ↔ shadow admin accounts. |
| Reschedule | Learner-facing class reschedule request form (/class-reschedule.html) — email OTP verification, enrolled-course picker (MMS course_runs + LMS lookup) and a manual fallback for learners with no matching enrolment. Saves a reschedule lead, notifies staff, and pushes WSQ requests to the LMS (non-fatal; falls back to pending_push). |
| MagentoCaptcha | Cloudflare Turnstile spam protection shared by the storefront lead/reschedule forms. |
| Courses / Leads | Course CRUD + admin grid; read-only catalog/schedule/search JSON APIs (api_wsq, api_nonwsq, api_courses, api_schedule, api_search, api_faq, api_contact) sharing one auth + envelope via Helper/Catalogfeed.php; contact-form lead capture with auto-reply, AI-drafted replies for every lead type — General Enquiry, Corporate Training, Franchise Enquiry and Customised Training (Claude drafts each reply with a type-specific brief, reviewers approve via a confirm-before-send link, approval auto-sends; all replies sign off with a unified support signature with clickable email/WhatsApp contacts) and MailerLite auto-subscribe on capture (opt-outs never re-added), plus a "Send to MailerLite" mass-action and per-lead sync checkbox in the Leads grid. |
| Marketing | Autonomous agentic-flyer newsletter pipeline — cron design (Mon/Thu 10am), signed email + backend manager approval, guarded MailerLite scheduling (Blastguard: 2 blasts/week, Mon/Thu 8am), subscriber-growth + campaign KPIs. Also hosts the daily 4am order-email → MailerLite subscriber sync (Model/Cron/Subscribersync.php, backfill scripts/maintenance/mailerlite-import-order-emails.php) — per-site config, opt-outs never re-added. |
| Reviews | Featured reviews as storefront testimonials — an is_featured flag on the stock review table (no parallel testimonial table), driven from the All Reviews grid (Featured column + filter + "Mark as Featured" mass action), the review edit form, and a dedicated Course Reviews → Featured Reviews admin page (curated list + KPIs + one-click remove). Renders 4 random featured reviews on the homepage after the blog strip, and all of them at /testimonials. |
| Blog | CMS-style lead-magnet blog — Marketing → Blog admin with the agentic pipeline timeline (research agent → writer + auto hero → approval → Tue/Fri 9am slots → LinkedIn/Facebook share), a recent-posts table (title, linked course codes, stage, schedule, likes, preview/approve actions), a drag-to-reorder next-course queue, and a rich in-admin article preview modal (desktop/mobile viewports, works for unpublished posts); /blog/<slug> storefront with likes + share. |
- Docker Desktop installed and running
- A base database dump (
courses_mysql2.sql) placed in the project root
# 1. Clone
git clone https://github.com/alfredang/ai-mms.git
cd ai-mms
# 2. Configure environment
cp .env.example .env # set MySQL passwords + API keys
cp app/etc/local.xml.example app/etc/local.xml # match DB credentials to .env
# 3. Build and start the stack
docker compose up -d --build
# 4. Import the base database
docker exec -i ai-mms-db_mysql-1 \
mysql -u root -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" < courses_mysql2.sql
# 5. Apply migrations (schema + data) via the real runner
docker exec ai-mms-web-1 php /var/www/html/migrations/apply.php
# 6. Apply local-dev fixups (localhost URLs, disable admin captcha, enable products)
for f in scripts/local-dev/*.sql; do
docker exec -i ai-mms-db_mysql-1 mysql -u root -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" < "$f"
done
# 7. Install Composer dependencies + clear cache
docker exec ai-mms-web-1 bash -c 'cd /var/www/html && composer install --no-dev --optimize-autoloader'
docker exec ai-mms-web-1 bash -c 'rm -rf /var/www/html/var/cache/* /var/www/html/var/full_page_cache/*'| Service | URL |
|---|---|
| Storefront | http://localhost:8080/ |
| Admin Panel | http://localhost:8080/`/ (the frontNameis set inapp/etc/local.xml`) |
| MySQL | localhost:3307 (credentials in .env) |
| Service | Image | Port |
|---|---|---|
web |
PHP 8.2 / Apache 2.4 | 8080 |
db_mysql |
MySQL 5.7 | 3307 |
Every franchise partner's server deploys automatically via its own Coolify instance on every push to main (each connected through Coolify's GitHub App git source):
- Coolify rebuilds the image from the pushed commit.
docker/entrypoint.shclears Magento runtime cache, then runsmigrations/apply.php(with retry/backoff while the DB comes up).- If migrations fail, the container exits non-zero so Coolify keeps the previous container — traffic is never served against a stale schema.
- Build timestamp is written to
/version.txt; public migration status at/media/migrations-status.json.
User-uploaded media (catalog product/category galleries) is served from Cloudflare R2; theme-baked assets ship inside the image.
Note for contributors: never
git pushuntil localhost is verified error-free — production redeploys on every push. Lint changed PHP, confirm rewrites instantiate, hit affected routes, and dry-run new migrations throughmigrations/apply.php(not themysqlclient) before pushing.
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-change - Make your changes and verify locally (lint + route checks + migration dry-run)
- Commit and open a Pull Request
Issues and feature requests are welcome via GitHub Issues.
This repo ships its AI coding tooling in-repo so every team member uses the same setup. Committed and ready to use after a clone:
.claude/skills/— domain skills (openmage-code-reviewer,openmage-module-developer,openmage-frontend-developer,backend-design,seo-audit,lead-magnets,add-country-store,recommended-courses,linkedin-posts,malaysia-blog,mysql,web-accessibility, …)..claude/agents/— specialised subagents (security auditor, caching/speed optimiser, mysql tuner, admin-design-consistency, site-health-checker,newsletter-designer, …)..claude/hooks/— pre/post-tool hooks (PHP lint-on-edit, web-health, leads-capture and newsletter-flyer verifiers) wired via.claude/settings.json..codex/+AGENTS.md— the Codex-CLI counterparts of the same guidance.
CLAUDE.md / AGENTS.md are the behavioural guardrails both assistants load automatically.
Tertiary Infotech Academy Pte. Ltd. 🌐 tertiarycourses.com.sg · ✉️ enquiry@tertiaryinfotech.com · ☎️ +65 6100 0613
- OpenMage LTS — the maintained Magento 1 fork this platform is built on
- Infortis Ultimo — storefront theme
- SkillsFuture Singapore (SSG), IBF, and HRD Corp — funding frameworks the catalogue aligns to
🤝 Bring this academy to your country — apply to become a franchise partner »
⭐ Star this repo if you find it useful!
