Every month the top student in Starters, Movers, Flyers, KET and PET on ranking.flyer.us wins a bicycle. Part A runs by hand from September 2026, with no development. Part B is the audit of today's leaderboard and the full design, to build only if the event survives the pilot.
Small monthly event first, system laterwebs-ranking, exam and flyer-go repositories, so every claim checks against a file path. Sections 5 onward are a proposal. What is not decided sits in Open questions.Start now with what exists, rather than wait for a system. Part A needs no developer: a one-page rule text, a printable teacher letter, one CS owner, a Google Sheet, a budget line. The board itself stays as it is.
This is the text a parent reads. It is deliberately short. Anything not here is decided by FLYER and explained to the family concerned.
| Rule | Text |
|---|---|
| Prize | Every month, the student ranked first on the Monthly tab of ranking.flyer.us in each of Starters, Movers, Flyers, KET and PET receives one bicycle. Five bicycles a month. |
| When | Standings are taken at 23:59 on the last day of the month, Vietnam time. FLYER keeps a screenshot as the record. Tests submitted after that belong to the next month. |
| Who can win | A student on their own personal account; class, centre, teacher and family-shared accounts cannot win. Age on the last day of the month must be inside the band: Starters 6–8, Movers 8–10, Flyers 10–12, KET 11–14, PET 13–16. |
| Teacher letter | Within 7 days the family sends a photo of the letter signed by the student's current English teacher, stating name, date of birth, school and class, that the student is in the age band, and sat the tests personally. FLYER may call the teacher. |
| Reserves | If the first-ranked student cannot be reached, does not send the letter in time, or is not eligible, the prize passes to rank 2, then rank 3. |
| Limits | One bicycle per student per month (if first in two levels, the higher level counts). One bicycle per student per calendar year. |
| Fair play | FLYER may set aside a result that shows signs of abuse, for example several accounts on one device, more than 20 full tests in a day, or a birthday changed during the month, and pass the prize to the next reserve. |
| Announcement | Winners are announced on the 12th of the following month on FLYER's Zalo and Facebook, with first name, school and province. Delivery within two weeks of the announcement. |
| Exclusions | FLYER staff, partner and school administrator accounts, and demonstration accounts are not eligible. |
One owner, about one working day per month. Times are Vietnam time.
| When | Task | Time |
|---|---|---|
| Last day, 23:50 | Open ranking.flyer.us, Monthly tab, each of the five levels. Screenshot the top 5 of each. Save to the Drive folder for the month. | 15 min |
| 1st, 08:00 to 09:00 | Screenshot again (the hourly refresh may have moved a late submission). The 08:00 screenshot is the record if the two differ. Fill the Sheet: level, rank 1 to 3, display name, score, profile id. | 30 min |
| 1st | In admin, open each of the 15 profiles: personal account or class account, birthday on file, school, parent phone. Note obvious class or centre names. Mark provisional winner and reserves. | 45 min |
| 1st to 2nd | Zalo or call the parent of each provisional winner (message A3.1). Send the letter template as PDF. Tell reserves they are reserves (message A3.2). | 1 h |
| 2nd to 8th | Receive letters. Check name and date of birth against the profile and the age band. Call the teacher on the number given (script A3.3). Log the call in the Sheet. | 2 h |
| 8th to 10th | Decide: winner confirmed, or passed to reserve with a reason. Product lead signs off in the Sheet. Ask the winner's family for address, bicycle size, and consent to publish first name, school and photo. | 45 min |
| 12th | Marketing posts the five winners (message A3.4). CS orders the bicycles. | 30 min |
| By the 26th | Delivery. Family sends a photo; posted with consent. Sheet row closed. | 30 min |
One tab per month, one row per level and rank (15 rows). Columns cover four groups: identity (level, rank, screenshot, display name, profile id, account type), age (birthday on file, age at month end, in band), contact and letter (parent, phone, contacted on, letter received and link, teacher name and phone, call date and outcome), and close-out (decision, decided by, address, bicycle size, consent, ordered, delivered, photo, notes).
A printable one-page bilingual form is at roadmap.flyer.vn/ranking-prize-letter. CS exports it to PDF once and sends the same file to every family, then takes back a phone photo of the signed page. The field list is in section 7.2.
| Risk | How the pilot handles it |
|---|---|
| Rank 1 is a class or centre account (as on the board today) | Not eligible under “own personal account”. The prize passes to the reserve. CS can usually tell from the display name; the teacher letter settles the rest. |
| The winner is a student who sat several hundred tests | Accepted for the pilot if they are a real child in the band; the fair-play clause covers extreme daily volumes. Two such winners at one level trigger the best-ten query for the next month. |
| No birthday on the profile (58% of August racers) | The letter carries the date of birth and the teacher confirms it. Ask the family to add it to the profile as part of the claim. |
| Same student first in two levels | Higher level counts; the lower level's prize passes to its reserve. |
| The board moved between 23:59 and the 08:00 screenshot | The 08:00 screenshot on the 1st is the record, because it contains every test submitted before midnight. Say so in the rules post. |
| Dispute from a family | Reply with the screenshot and the rule text. The rules say FLYER's decision is final. |
| Teacher unreachable | Two attempts on two days, then the school's office; if still nothing by the 10th, pass to the reserve and tell the family why. |
The five backlog items on the board are ordered for that moment and stay parked until then.
Each month the student with the top score in Starters, Movers, Flyers, KET and PET receives a bicycle. One condition came with the request: the winner presents a letter from their English teacher confirming they are within the Cambridge age range for that level.
The leaderboard already exists and is well used. What it lacks is everything a physical prize needs: a formula that is written down and versioned, eligibility rules, a way to tell a real child from an anonymous or duplicate profile, an age on the profile, and tooling to run a claim.
A bicycle is worth enough that families will optimise for it. Section 4 lists the specific gaps; the consequence is that the first month of prizes would produce disputes the team cannot adjudicate, because the rule that produced the number is not in any repository.
| Metric | Definition | Type |
|---|---|---|
| Active racers per level | Distinct eligible students with at least one counted test in the month, per level | Primary |
| Tests per racer | Median counted tests per active racer in the month | Primary |
| Profiles with verified DOB | Share of active student profiles with a date of birth entered and locked | Primary, the lasting asset |
| Claim completion | Share of provisional winners who upload a valid letter within the claim window | Guardrail |
| Verification turnaround | Days from freeze to announcement, target 12 | Guardrail |
| Disputes | Complaints per cycle that require a rule interpretation | Guardrail, target zero after month 2 |
| Retention | Share of month N racers still active in month N+1 | The real test of whether a prize changes behaviour |
| Level | Active students | 1 test | 2 to 4 | 5 to 9 | 10 or more |
|---|---|---|---|---|---|
| Starters | 11,950 | 4,181 | 3,553 | 1,899 | 2,317 |
| Movers | 9,954 | 3,463 | 2,881 | 1,615 | 1,995 |
| Flyers | 9,325 | 3,484 | 2,635 | 1,392 | 1,814 |
| KET | 6,077 | 2,227 | 1,775 | 1,085 | 990 |
| PET | 4,265 | 1,752 | 1,344 | 673 | 496 |
Read from the source, not assumed. The live app is flyer/webs-ranking (Next.js, last commit 27/08/2026) served at ranking.flyer.us; the older ranking-web repository is dead since October 2024. The ranking API is GraphQL v2 in flyer/exam.
| Aspect | Current behaviour | Where |
|---|---|---|
| Time windows | Daily, Weekly, Monthly, Yearly. No all-time. Default tab is Daily. | RankFrequency enum, pages/index.tsx |
| Levels | A dropdown of test levels of type CLASSIC: Starters (default), Movers, Flyers, KET, PET, plus IOE, SET, TOEFL Primary and Junior. IELTS is its own type and does not appear. | TestLevelsWithFilter |
| Tables | Student table and School table. Search box switches to challenge code (digits), class code (CL…) or a name search. | TableRanking |
| Score shown | The grade column: a sum of per-test grades normalised to 10 points each, over the window. Not a best score and not an average. A student who takes 40 tests outranks a student who takes 10 perfect ones. | learner_test_level_ranks |
| Where the formula lives | A materialized view created by hand on the production database. It is declared in Go only as a column list. No CREATE VIEW exists in any repository, so the exact tie-break cannot be read from git. | ent/schema/views.go, module/mview |
| Refresh | Hourly via Temporal, Asia/Ho_Chi_Minh. A submitted test can take up to an hour to move the board. | module/mview/mview.go |
| Which attempts count | Full mock tests only (type_attempt = 'exam'), submitted, grade above zero. Practice, speaking, writing and placement attempts do not count. Teacher-assigned and self-served tests count the same. No sample or paid filter. | sibling views in flyer-go |
| Tie-break | By analogy with the versioned monthly recap view: score descending, then total execution time ascending. Unverified for the leaderboard view itself. | 0092_user_score_views.up.sql |
| Filters | Province, school, class code, challenge code, display name. No age, grade or birth-year filter exists on the API. | LearnerTestLevelRankFilter |
| Identity shown | Rank, avatar and frame, display name, province, school, score. Clicking any row opens a public profile modal with coins, total points, study time, completed tests, accuracy, achievements and full attempt history. | ModalInfoProfileRanking |
| Date of birth | Fetched in the ranking fragment and never rendered. On the profile it is nullable, unvalidated and never read by any ranking, recap or badge job. | learner_profiles.date_of_birth |
| Eligibility | None. Anonymous profiles rank, one user can hold several profiles, and staff and test accounts are not excluded. Demo students have their attempt dates shifted forward hourly, so they surface in Daily and Weekly rankings. | AuthProvider, IsAnonymous, update_attempt_dates_by_students |
| Rewards today | One badge, TOURNAMENT_STAR, granted every Monday 01:00 to every learner holding rank 1 in any level for the week. Wisdom-stone gift store and giveaway campaigns exist but are unrelated to rank. No prize system, no admin screen for prizes. | AchievementWeeklyRankSt |
| Known UI defect | Medal icons for the top three are keyed off the row index, not the server rank. With any filter applied, the first three rows of the filtered list get gold, silver and bronze even if their real rank is 5,000. | TableRanking/index.tsx |
| # | Gap | What it costs the prize |
|---|---|---|
| 1 | Formula not in version control | Any dispute ends with “the database says so”. The team cannot show a parent the rule, cannot test it on dev (dev and prod already drift on the sibling monthly view), and cannot change it with a review trail. |
| 2 | Volume beats quality | A running sum means the winner is whoever sits the most tests: in August 2026 the number one at KET submitted 849 attempts and the number one at Movers sat 602 distinct tests, roughly twenty a day. Neither the behaviour FLYER wants nor a result Academic can stand behind. |
| 3 | No eligibility | Anonymous profiles, duplicate profiles, staff, school demo accounts and artificially refreshed sample students are all rankable. At least one of these will be in a top three in month one. |
| 4 | No age on record | The condition “right Cambridge age for the level” cannot be checked, because date of birth is optional and unvalidated, and nothing in the API can filter on it. |
| 5 | No claim or verification tooling | There is nowhere for a winner to upload a letter, nowhere for CS to approve it, and no record of who won, when, and on what evidence. |
| 6 | Hourly lag and index-based medals | Small on their own, but on the last evening of the month a student who sees themselves in first place at 23:30 and third at 00:30 will not accept “the view had not refreshed”. |
| Level | Attempts | Distinct tests |
|---|---|---|
| Starters | 822 | 125 |
| Movers | 680 | 602 |
| Flyers | 444 | 353 |
| KET | 849 | 549 |
| PET | 311 | 204 |
The rules as a parent should be able to read them. Sections 9 to 14 exist to make these rules true.
| Level | Students at 100 / 100 |
|---|---|
| Starters | 641 |
| Movers | 410 |
| Flyers | 372 |
| KET | 163 |
| PET | 61 |
Cambridge publishes the Young Learners suite for children aged roughly 6 to 12, but publishes no fixed age for KET and PET for Schools. The bands below are FLYER's operational bands, aligned to that guidance and to Vietnamese school grades; age is counted in completed years on the last day of the prize month.
| Level | Cambridge guidance | FLYER band (age at month end) | Typical VN grade | Born in, for a September 2026 race |
|---|---|---|---|---|
| Starters (Pre A1) | YLE, children 6 to 12; Starters typically 7 to 8 | 6 to 8 | Grade 1 to 3 | 2018 to 2020 |
| Movers (A1) | Typically 8 to 11 | 8 to 10 | Grade 3 to 5 | 2016 to 2018 |
| Flyers (A2) | Typically 9 to 12 | 10 to 12 | Grade 5 to 7 | 2014 to 2016 |
| KET (A2 Key for Schools) | School-aged; commonly lower secondary | 11 to 14 | Grade 6 to 9 | 2012 to 2015 |
| PET (B1 Preliminary for Schools) | School-aged; commonly upper secondary | 13 to 16 | Grade 8 to 11 | 2010 to 2013 |
The platform can compute an age from a date of birth, but cannot know whether the date is true or whether the child sat the tests. A named teacher who signs a short form answers both, and gives CS one phone number to call.
| Field | Purpose |
|---|---|
| Student full name, date of birth, school, grade and class | Matched against the profile; a mismatch on date of birth is an automatic reject. |
| Level won, prize month | Ties the letter to one cycle so it cannot be reused. |
| Statement 1: the student is within the FLYER age band for this level | The condition the programme was built on. |
| Statement 2: the student is currently learning English at or around this level | Catches an eleven-year-old parked in Starters for the prize even if the age fits the overlap. |
| Statement 3: to the teacher's knowledge the student completed the tests personally | The integrity check; makes the teacher a witness, not just a clerk. |
| Teacher full name, role, school or centre, mobile number, signature, date; school stamp if available | Gives CS a person to call. The stamp is optional because centre teachers often cannot obtain one. |
| Parent or guardian name, phone, signature, and consent to publish the winner's first name, school and photo | Required by the winner wall and by the delivery. |
The template is a one-page PDF, bilingual, downloadable from the claim page. A photo of the signed page taken with a phone is accepted; the upload is limited to 10 MB and to JPG, PNG, HEIC and PDF.
One cycle per level per month, all five running on the same calendar. Steps 1 and 2 are the existing leaderboard with the new formula; steps 3 to 7 are new.
Replace the hand-made monthly ranking for the five prize levels with a view whose definition lives in a migration file, computes the best-ten score in section 5.2, and carries a version number that is printed on the board.
monthly_prize_ranks (level, month, learner profile, score, distinct test count, capped total time, time of final score, rank, formula version), added to the hourly refresh list next to learner_test_level_ranks.row_number), never a percentile bucket. The sibling monthly recap view uses ntile(100) on production and row_number() on dev; that drift must not be repeated here.championship_month and championship_level, hidden until its window opens, single attempt enforced server-side, and released into the normal library afterwards. Academic delivers five such tests by the 20th of each month.WITH counted AS ( -- one row per (student, test) = best attempt that month
SELECT ua.learner_profile_id, ua.test_level_id, ua.check_point_id,
bool_or(cp.championship_month IS NOT NULL) AS is_championship,
max(round(10 * ua.grade / ua.max_grade, 2)) AS best_grade,
min(least(ua.execution_time, tl.execution_time)) AS best_time,
min(ua.submitted_at) AS first_at
FROM user_attempts ua
JOIN test_level_exam_settings tl ON tl.test_level_id = ua.test_level_id
JOIN check_points cp ON cp.id = ua.check_point_id
WHERE ua.type_attempt = 'exam' AND ua.submitted_at IS NOT NULL
AND ua.grade > 0 AND ua.max_grade > 0
AND (ua.submitted_at AT TIME ZONE 'Asia/Ho_Chi_Minh') >= date_trunc('month', now() AT TIME ZONE 'Asia/Ho_Chi_Minh')
GROUP BY 1,2,3
), ranked AS (
SELECT *, row_number() OVER (PARTITION BY learner_profile_id, test_level_id
ORDER BY best_grade DESC, first_at) AS k
FROM counted WHERE NOT is_championship
), best10 AS (
SELECT learner_profile_id, test_level_id,
sum(best_grade) FILTER (WHERE k <= 10) AS score10,
count(*) AS distinct_tests,
sum(best_time) FILTER (WHERE k <= 10) AS counted_time,
max(first_at) FILTER (WHERE k <= 10) AS final_score_at
FROM ranked GROUP BY 1,2
), champ AS ( -- the month's single-attempt championship test
SELECT learner_profile_id, test_level_id, best_grade * 10 AS champ_score, best_time AS champ_time
FROM counted WHERE is_championship
)
SELECT b.learner_profile_id, b.test_level_id,
b.score10 + coalesce(c.champ_score, 0) AS score,
b.score10, c.champ_score, b.distinct_tests, b.counted_time, c.champ_time, b.final_score_at,
row_number() OVER (PARTITION BY b.test_level_id
ORDER BY b.score10 + coalesce(c.champ_score, 0) DESC,
c.champ_time ASC NULLS LAST, b.distinct_tests DESC,
b.counted_time ASC, b.final_score_at ASC) AS rank,
1 AS formula_version
FROM best10 b LEFT JOIN champ c USING (learner_profile_id, test_level_id);
exam repository and produces the same rank on dev and production for the same fixture data.dob_locked_at). Changing it requires a support ticket with a teacher letter or birth certificate. This is what makes the age check meaningful.A nightly job (and again at freeze) writes an eligibility row per active student and level: eligible or not, with a list of reasons. Reasons are a fixed enum so the UI can explain them.
| Reason code | Meaning | Shown to the student as |
|---|---|---|
ANONYMOUS | Profile has no user account | Create an account to be eligible |
NO_PARENT_PHONE | No verified parent phone | Add a parent's phone number |
NO_DOB | No date of birth, or added after the 20th | Add your birthday before the 20th |
AGE_OUT_OF_BAND | Age at month end outside the level's band | This level is for ages 8 to 10; you can race in Flyers |
EXCLUDED | On the exclusion list (staff, demo, partner admin, integrity hold) | Not eligible for prizes |
SECONDARY_PROFILE | User has another profile marked primary | Only your main profile can win |
WON_THIS_YEAR | Already received a bicycle this calendar year | You already won in March, congratulations; next year again |
@flyer.us or @flyer.vn user, the configured demo student ids that the sample-class job shifts dates for, and profiles owned by partner admin accounts.A page on ranking.flyer.us reachable from the notification and from the board banner, for the logged-in provisional winner or reserve.
invited, draft, submitted, under_review, verified, rejected, expired, awarded, delivered. Each state change notifies the parent./prize/winners lists past winners by month and level: first name, school, province, score, delivery photo where consented./prize/rules renders section 5 and section 6 of this document, bilingual, with the current formula version.A list in admin-web of prize cycles, one row per month and level, with status (open, frozen, claiming, verifying, announced, closed), the frozen top ten, and the claim chain (winner, reserve 1, reserve 2 and who currently holds the prize).
A plain list, filterable by status and level. Each claim opens to a single screen: profile card, frozen standings row with the ten counted tests, the uploaded letter, the teacher's phone, integrity signals (section 13), a call log, and Approve or Reject. The cycle turns to announced once all five levels are verified, or on the 12th.
An exclusion list with add, reason, expiry and an audit log. A manual override to pass the prize to a reserve outside the automatic rules, with a mandatory note; overrides are listed on the cycle for later review.
If the student belongs to a class on FLYER Teacher or Teado.ai, the claim names that teacher, who confirms in-product with one tap. The claim records the teacher's account, which is stronger than a phone call. Paper remains for students without a FLYER teacher.
CSV of winners per cycle for finance (prize value, delivery cost) and a monthly report of claims by outcome and reason for the product review.
A bicycle attracts effort. Rules alone are not enough; the platform must surface signals so that CS reviews with evidence, and every decision must be reconstructible.
| Signal | Rule | Effect |
|---|---|---|
| Implausible speed | A counted test with grade 10 completed in under 25% of the level's allotted time | Flag on the claim; CS asks the teacher specifically about it |
| Multi-profile user | The user account owns more than one student profile with attempts this month | Only the primary profile is eligible; flag for review |
| Shared device or network | Several top-20 profiles at the same level share a device fingerprint or the same network across most attempts | Flag; does not auto-reject, because siblings and classrooms exist |
| Night-time bursts | More than five counted tests submitted between 00:00 and 05:00 in the month | Flag |
| Late birthday | Date of birth added within ten days of the freeze | Automatic NO_DOB for that month |
| Answer-pattern similarity | Two profiles with identical answer sequences on three or more tests | Flag both; integrity hold if confirmed |
exam repository)| Table | Key columns | Notes |
|---|---|---|
prize_cycles | id, month, test_level_id, status, formula_version, frozen_at, announced_at, closed_at | One row per level per month, created by the freeze job |
prize_standings | cycle_id, learner_profile_id, rank, score, distinct_tests, counted_time, final_score_at, eligible, reasons (jsonb), counted_tests (jsonb) | Frozen snapshot of the top 50 per level plus every eligible student's own row; immutable |
prize_eligibility | learner_profile_id, test_level_id, month, eligible, reasons (jsonb), computed_at | Nightly; drives the badge on the board |
prize_claims | id, cycle_id, learner_profile_id, position (winner / reserve1 / reserve2), status, letter_file, teacher_name, teacher_role, teacher_org, teacher_phone, parent_name, parent_phone, address, bike_size, consent_publish, reviewer_email, reject_reason, deadline_at, timestamps | One per invited student per cycle |
prize_claim_events | claim_id, actor, action, detail, created_at | Append-only audit log including call outcomes |
prize_exclusions | learner_profile_id or user_id, reason, expires_at, created_by | Seeded from staff domains and the demo student list |
prize_age_bands | test_level_id, min_age, max_age, effective_from | Academic-owned; history kept |
learner_profiles.dob_locked_at, learner_profiles.is_primary (one per user, default the oldest profile with attempts).date_of_birth: range check, and reject updates once locked unless the caller has the support role.monthly_prize_ranks alongside the existing views.prize_eligibility for the current month.prize_cycles and prize_standings, create claims for the winner and two reserves per level, send notifications.monthlyPrizeRanks(testLevelId, month, first, after, filterBy) returning score, distinct tests, rank, eligible and reasons; viewerPrizeStatus(testLevelId) for the checklist; prizeCycle(month, testLevelId) for the banner and winner wall.GET /v4/student/prize/claims/current, PUT /v4/student/prize/claims/{id}, POST /v4/student/prize/claims/{id}/letter (multipart), POST /v4/student/prize/claims/{id}/submit.webs-ranking: Monthly tab reads the new query for the five levels; eligibility badge and checklist; prize banner; pages /prize/claim/[month]/[level], /prize/winners, /prize/rules; fix the index-based medals.admin-web: /center/prizes/cycles, /center/prizes/claims, /center/prizes/exclusions, /center/prizes/age-bands, following the plain-list pattern of the wisdom-stone screens.| Phase | Contents | Outcome |
|---|---|---|
| Phase 1, first cycle | All four requirements A to D in sections 9 to 12, plus the freeze and notification jobs | The first month of prizes runs on rules a parent can read and a CS agent can enforce |
| Phase 2 | In-product teacher confirmation via FLYER Teacher and Teado.ai, integrity signals in the claim screen, answer-pattern similarity, reserve pre-submission, finance export | Verification takes hours instead of days and stops relying on phone calls |
| Phase 3 | Apply the versioned formula to Weekly and Yearly, school and province sub-prizes, weekly mini-prizes in wisdom stones | One formula family across every tab |
| Out of scope | Cash prizes, prizes for IELTS or TOEFL levels, changes to the Daily tab, changes to the wisdom-stone store |
| Item | Assumption | VND per month |
|---|---|---|
| Bicycles | 5 × 2,500,000 to 3,500,000 (child and junior sizes) | 12,500,000 to 17,500,000 |
| Delivery | 5 × 150,000 to 400,000, nationwide | 750,000 to 2,000,000 |
| CS time | 15 claims including reserves × 30 minutes | about 1 working day |
| Total | roughly 14,000,000 to 20,000,000 |
Bicycle sizes differ across the age bands: a six-year-old and a sixteen-year-old do not ride the same bicycle. The claim form asks for a size, and procurement should hold two models.
| Question | Why it affects the design | Decider |
|---|---|---|
| Confirm the age bands in section 6, in particular KET 11 to 14 and PET 13 to 16. | Cambridge publishes no fixed age for the for-Schools papers; these are FLYER's choice and they decide who can win. | Academic |
| Admit students younger than the band? | A seven-year-old winning Flyers is good marketing and not unfair to older students. Excluding them is simpler to explain. The default in this draft is to exclude. | Academic + Product |
| Ten tests, or a different cap? | Sets the effort floor. Ten is about four to five hours at Starters; if Academic thinks that is too much for six-year-olds, Starters could use eight. | Academic |
| Can Academic deliver five fresh, unpublished full tests every month? | The championship test only works if the content has never been seen. If capacity is the constraint, the window could reuse a test drawn at random from a sealed pool of twenty per level. | Academic + Content |
| Championship window: the whole last weekend, or Sunday only? | A longer window is fairer to families with weekend classes; a shorter one makes answer sharing harder. The draft uses Saturday 08:00 to Sunday 22:00. | Product + Academic |
| Should the versioned formula also replace Daily, Weekly and Yearly from day one? | Cleaner, but changes numbers students already watch and widens phase 1. The draft keeps it to the Monthly tab of the five prize levels. | Product |
| Is a parent phone number available on enough student profiles? | If most profiles lack one, requiring it in month one will disqualify most of the top ten. Needs a replica query. | Product + Data |
| Who signs for home-schooled students or students without an English teacher? | The draft accepts centre teachers and homeroom teachers for grades 1 to 3, but has no answer for a child with neither. | CS + Academic |
| Publish the full score breakdown of winners publicly? | Transparency reduces disputes but exposes which tests a child took. The draft shows the breakdown to the claimant and to admin only. | Product + Legal |
| Budget approval and procurement of two bicycle models. | Roughly 14 to 20 million VND a month before marketing. | Finance |
flyer/webs-ranking (src/pages/index.tsx, src/components/TableRanking, src/api/common.ts, src/providers/auth), flyer/exam (graph/schema/type_extended.graphql, graph/user/type_extended.resolvers.go, ent/schema/views.go, module/mview, module/learnerprofile) and flyer/flyer-go (db/migrations/base/0092_user_score_views.up.sql, service/student/db/queries/score.sql). The exact production definition of learner_test_level_ranks must be read from pg_matviews; it is in no repository.Mỗi tháng, học sinh đứng đầu Starters, Movers, Flyers, KET và PET trên ranking.flyer.us nhận một chiếc xe đạp. Phần A chạy tay từ tháng 9/2026, không cần lập trình. Phần B là bản rà soát hiện trạng và thiết kế đầy đủ, chỉ xây nếu sự kiện được giữ sau ba tháng thử.
Sự kiện nhỏ hằng tháng trước, hệ thống sauwebs-ranking, exam, flyer-go nên kiểm chứng được theo đường dẫn tệp. Từ mục 5 trở đi là đề xuất. Điểm chưa quyết nằm ở Câu hỏi mở.Bắt đầu ngay với những gì đang có, không chờ hệ thống. Phần A không cần lập trình viên: một thể lệ một trang, một mẫu giấy xác nhận in được, một người CS, một Google Sheet, một dòng ngân sách. Bảng xếp hạng giữ nguyên.
Đây là nội dung phụ huynh đọc, cố ý ngắn. Điều gì không có ở đây do FLYER quyết định và giải thích cho gia đình.
| Quy tắc | Nội dung |
|---|---|
| Giải thưởng | Học sinh hạng nhất tab Tháng ở mỗi cấp độ Starters, Movers, Flyers, KET, PET nhận một chiếc xe đạp. Năm xe mỗi tháng. |
| Thời điểm | Bảng xếp hạng chốt lúc 23:59 ngày cuối tháng, giờ Việt Nam. FLYER lưu ảnh chụp màn hình làm bằng chứng. Bài nộp sau thời điểm đó thuộc về tháng sau. |
| Ai được thắng | Học sinh dùng tài khoản cá nhân của chính mình; tài khoản lớp, trung tâm, giáo viên và dùng chung không được thắng. Tuổi vào ngày cuối tháng phải trong khung: Starters 6–8, Movers 8–10, Flyers 10–12, KET 11–14, PET 13–16. |
| Giấy xác nhận của giáo viên | Trong 7 ngày, gia đình gửi ảnh giấy xác nhận có chữ ký của giáo viên tiếng Anh hiện tại, ghi họ tên, ngày sinh, trường, lớp, xác nhận học sinh trong khung tuổi và tự làm bài. FLYER có thể gọi giáo viên. |
| Dự bị | Nếu học sinh hạng nhất không liên lạc được, không gửi giấy đúng hạn hoặc không đủ điều kiện, giải chuyển cho hạng 2, rồi hạng 3. |
| Giới hạn | Mỗi học sinh một xe đạp mỗi tháng (nếu đứng nhất ở hai cấp độ, tính cấp độ cao hơn). Mỗi học sinh một xe đạp mỗi năm dương lịch. |
| Công bằng | FLYER có thể không công nhận kết quả bất thường (nhiều tài khoản một thiết bị, hơn 20 đề một ngày, ngày sinh đổi trong tháng) và chuyển giải cho dự bị. |
| Công bố | Người thắng được công bố ngày 12 tháng kế tiếp trên Zalo và Facebook của FLYER, với tên, trường và tỉnh thành. Giao xe trong hai tuần sau công bố. |
| Loại trừ | Nhân viên FLYER, tài khoản quản trị đối tác và nhà trường, tài khoản demo không được tham gia. |
Một người phụ trách, khoảng một ngày công mỗi tháng. Giờ Việt Nam.
| Khi nào | Việc | Thời gian |
|---|---|---|
| Ngày cuối tháng, 23:50 | Mở ranking.flyer.us, tab Tháng, lần lượt năm cấp độ. Chụp top 5 của mỗi cấp độ. Lưu vào thư mục Drive của tháng. | 15 phút |
| Ngày 1, 08:00 đến 09:00 | Chụp lại, vì lần làm mới mỗi giờ có thể đã cộng bài nộp muộn; ảnh 08:00 là bằng chứng. Điền Sheet: cấp độ, hạng 1 đến 3, tên hiển thị, điểm, id hồ sơ. | 30 phút |
| Ngày 1 | Mở 15 hồ sơ trong trang quản trị: loại tài khoản, ngày sinh, trường, số phụ huynh. Đánh dấu người thắng tạm thời và dự bị. | 45 phút |
| Ngày 1 đến 2 | Nhắn Zalo hoặc gọi phụ huynh của mỗi người thắng tạm thời (mẫu A3.1). Gửi mẫu giấy dạng PDF. Báo dự bị biết họ là dự bị (mẫu A3.2). | 1 giờ |
| Ngày 2 đến 8 | Nhận giấy. Đối chiếu tên, ngày sinh với hồ sơ và khung tuổi. Gọi giáo viên theo số đã ghi (kịch bản A3.3). Ghi cuộc gọi vào Sheet. | 2 giờ |
| Ngày 8 đến 10 | Xác nhận người thắng hoặc chuyển dự bị kèm lý do, trưởng sản phẩm ký duyệt trong Sheet. Hỏi gia đình địa chỉ, cỡ xe và đồng ý công bố. | 45 phút |
| Ngày 12 | Marketing đăng năm người thắng (mẫu A3.4). CS đặt xe. | 30 phút |
| Trước ngày 26 | Giao xe. Gia đình gửi ảnh; đăng khi được đồng ý. Đóng dòng trong Sheet. | 30 phút |
Mỗi tháng một tab, mỗi cấp độ và hạng một dòng (15 dòng). Các cột: cấp độ, hạng, link ảnh chụp, tên hiển thị, id hồ sơ, loại tài khoản, ngày sinh trong hồ sơ, tuổi cuối tháng, trong khung (có/không), tên phụ huynh, số phụ huynh, ngày liên hệ, ngày nhận giấy, link giấy, tên giáo viên, số giáo viên, ngày gọi, kết quả cuộc gọi, quyết định, người quyết, địa chỉ, cỡ xe, đồng ý công bố, ngày đặt xe, ngày giao, link ảnh, ghi chú.
Mẫu một trang song ngữ, in được, tại roadmap.flyer.vn/ranking-prize-letter. CS xuất PDF một lần, gửi chung cho mọi gia đình, nhận lại ảnh chụp trang đã ký. Danh sách trường thông tin xem mục 7.2.
| Rủi ro | Cách giai đoạn thử xử lý |
|---|---|
| Hạng 1 là tài khoản lớp hoặc trung tâm (như trên bảng hôm nay) | Không đủ điều kiện, giải chuyển cho dự bị. CS thường nhận ra qua tên hiển thị; giấy xác nhận giải quyết phần còn lại. |
| Người thắng là học sinh làm vài trăm đề | Chấp nhận nếu là học sinh thật trong khung tuổi; điều khoản công bằng xử lý khối lượng cực đoan. Hai ca như vậy ở một cấp độ sẽ kích hoạt truy vấn mười bài tốt nhất. |
| Hồ sơ không có ngày sinh (58% học sinh đua trong tháng 8) | Giấy xác nhận ghi ngày sinh và giáo viên xác nhận. Đề nghị gia đình bổ sung vào hồ sơ như một phần của việc nhận giải. |
| Cùng một học sinh đứng nhất hai cấp độ | Tính cấp độ cao hơn; giải cấp độ thấp chuyển cho dự bị của cấp độ đó. |
| Bảng thay đổi giữa 23:59 và ảnh chụp 08:00 | Ảnh 08:00 ngày 1 là bằng chứng vì đã gồm mọi bài nộp trước nửa đêm. Ghi rõ trong bài đăng thể lệ. |
| Gia đình khiếu nại | Trả lời bằng ảnh chụp và nội dung thể lệ. Thể lệ ghi quyết định của FLYER là cuối cùng. |
| Không liên lạc được giáo viên | Hai lần trong hai ngày, rồi văn phòng trường; nếu đến ngày 10 vẫn không được, chuyển dự bị và báo gia đình lý do. |
Năm item backlog trên board được sắp cho thời điểm đó và tạm gác lại cho tới lúc ấy.
Mỗi tháng, học sinh điểm cao nhất ở Starters, Movers, Flyers, KET và PET nhận một chiếc xe đạp. Điều kiện kèm theo: người thắng nộp giấy xác nhận của giáo viên tiếng Anh, chứng nhận học sinh đúng độ tuổi Cambridge của cấp độ đã thắng.
Bảng xếp hạng đã có và được dùng nhiều. Thiếu là mọi thứ một giải thưởng hiện vật cần: công thức ghi thành văn và có phiên bản, quy tắc điều kiện dự thi, cách phân biệt học sinh thật với hồ sơ ẩn danh hoặc trùng, ngày sinh trên hồ sơ, và công cụ vận hành nhận giải.
Một chiếc xe đạp đủ giá trị để các gia đình tối ưu vì nó. Các lỗ hổng cụ thể nằm ở mục 4; hệ quả là tháng trao giải đầu tiên sẽ sinh tranh cãi mà đội ngũ không phân xử được, vì quy tắc tạo ra con số đó không nằm trong kho mã nào.
| Chỉ số | Định nghĩa | Loại |
|---|---|---|
| Số học sinh đua mỗi cấp độ | Học sinh đủ điều kiện có ít nhất một bài được tính trong tháng, theo cấp độ | Chính |
| Số bài mỗi học sinh | Trung vị số bài được tính của học sinh đang đua | Chính |
| Hồ sơ có ngày sinh đã xác minh | Tỷ lệ hồ sơ hoạt động có ngày sinh đã nhập và đã khoá | Chính, tài sản lâu dài |
| Tỷ lệ hoàn tất nhận giải | Tỷ lệ người thắng tạm thời nộp giấy hợp lệ trong thời hạn | Ràng buộc |
| Thời gian thẩm định | Số ngày từ lúc chốt đến lúc công bố, mục tiêu 12 | Ràng buộc |
| Tranh chấp | Khiếu nại mỗi chu kỳ cần diễn giải quy tắc | Ràng buộc, mục tiêu bằng không từ tháng 2 |
| Giữ chân | Tỷ lệ học sinh đua ở tháng N vẫn hoạt động ở tháng N+1 | Phép thử thật về việc phần thưởng có đổi hành vi không |
| Cấp độ | Học sinh hoạt động | 1 bài | 2 đến 4 | 5 đến 9 | 10 trở lên |
|---|---|---|---|---|---|
| Starters | 11.950 | 4.181 | 3.553 | 1.899 | 2.317 |
| Movers | 9.954 | 3.463 | 2.881 | 1.615 | 1.995 |
| Flyers | 9.325 | 3.484 | 2.635 | 1.392 | 1.814 |
| KET | 6.077 | 2.227 | 1.775 | 1.085 | 990 |
| PET | 4.265 | 1.752 | 1.344 | 673 | 496 |
Đọc từ mã nguồn, không suy đoán. Ứng dụng đang chạy là flyer/webs-ranking (Next.js, commit 27/08/2026); kho ranking-web cũ ngừng từ 10/2024. API xếp hạng là GraphQL v2 trong flyer/exam.
| Khía cạnh | Hành vi hiện tại | Vị trí |
|---|---|---|
| Khung thời gian | Ngày, Tuần, Tháng, Năm. Không có mọi thời điểm. Tab mặc định là Ngày. | enum RankFrequency, pages/index.tsx |
| Cấp độ | Các cấp độ loại CLASSIC: Starters (mặc định), Movers, Flyers, KET, PET, IOE, SET, TOEFL Primary và Junior. IELTS là loại riêng, không xuất hiện. | TestLevelsWithFilter |
| Bảng | Bảng học sinh và bảng trường. Ô tìm kiếm chuyển sang mã thử thách (chữ số), mã lớp (CL…) hoặc tìm theo tên. | TableRanking |
| Điểm hiển thị | Cột grade: điểm từng bài quy về thang 10, cộng dồn trong khung thời gian, không phải điểm cao nhất hay trung bình. Học sinh làm 40 bài xếp trên học sinh làm 10 bài tuyệt đối. | learner_test_level_ranks |
| Công thức nằm ở đâu | Một materialized view tạo tay trên production; mã Go chỉ khai báo danh sách cột. Không kho nào có CREATE VIEW, nên không đọc được quy tắc phân định đồng điểm từ git. | ent/schema/views.go, module/mview |
| Làm mới | Mỗi giờ qua Temporal, múi giờ Asia/Ho_Chi_Minh. Một bài vừa nộp có thể mất tới một giờ mới thay đổi bảng. | module/mview/mview.go |
| Bài nào được tính | Chỉ đề thi thử đầy đủ (type_attempt = 'exam'), đã nộp, điểm lớn hơn 0. Bài luyện, nói, viết, kiểm tra đầu vào không tính; bài giáo viên giao và bài tự chọn tính như nhau. | các view cùng họ trong flyer-go |
| Phân định đồng điểm | Suy từ view tổng kết tháng có phiên bản: điểm giảm dần, rồi tổng thời gian làm bài tăng dần. Chưa kiểm chứng được cho chính view bảng xếp hạng. | 0092_user_score_views.up.sql |
| Bộ lọc | Tỉnh thành, trường, mã lớp, mã thử thách, tên hiển thị. API không có bộ lọc theo tuổi, khối lớp hay năm sinh. | LearnerTestLevelRankFilter |
| Danh tính hiển thị | Hạng, ảnh đại diện, tên hiển thị, tỉnh thành, trường, điểm. Mở một dòng ra hồ sơ công khai: xu, tổng điểm, thời gian học, số bài, độ chính xác, thành tích và toàn bộ lịch sử làm bài. | ModalInfoProfileRanking |
| Ngày sinh | Có trong fragment xếp hạng nhưng không hiển thị. Trên hồ sơ, trường này cho phép rỗng, không kiểm tra, và không tác vụ nào đọc nó. | learner_profiles.date_of_birth |
| Điều kiện dự thi | Không có. Hồ sơ ẩn danh vẫn được xếp hạng, một người dùng giữ được nhiều hồ sơ, tài khoản nhân viên và thử nghiệm không bị loại. Học sinh demo được dời ngày làm bài mỗi giờ nên xuất hiện ở bảng Ngày và Tuần. | AuthProvider, IsAnonymous, update_attempt_dates_by_students |
| Phần thưởng hiện có | Một huy hiệu TOURNAMENT_STAR cấp 01:00 thứ Hai cho học sinh hạng 1 trong tuần. Cửa hàng đá tri thức và chiến dịch tặng quà không liên quan thứ hạng. Chưa có hệ thống hay màn quản trị giải thưởng. | AchievementWeeklyRankSt |
| Lỗi giao diện đã biết | Huy chương ba vị trí đầu gắn theo chỉ số dòng, không theo hạng máy chủ. Áp bộ lọc bất kỳ là ba dòng đầu nhận vàng, bạc, đồng dù hạng thật là 5.000. | TableRanking/index.tsx |
| # | Khoảng trống | Hệ quả với giải thưởng |
|---|---|---|
| 1 | Công thức không được quản lý phiên bản | Mọi tranh chấp kết thúc bằng “cơ sở dữ liệu nói vậy”: không cho phụ huynh xem được quy tắc, không thử được trên dev (dev và prod đã lệch nhau), không sửa được có dấu vết duyệt. |
| 2 | Số lượng thắng chất lượng | Tổng cộng dồn nghĩa là người thắng là người làm nhiều bài nhất: tháng 8/2026, hạng nhất KET nộp 849 lượt, hạng nhất Movers làm 602 bài khác nhau, khoảng hai mươi bài mỗi ngày. Không phải hành vi FLYER muốn, cũng không phải kết quả Học thuật bảo vệ được. |
| 3 | Không có điều kiện dự thi | Hồ sơ ẩn danh, hồ sơ trùng, nhân viên, tài khoản demo và học sinh mẫu đều được xếp hạng; ít nhất một trong số này sẽ lọt top 3 ngay tháng đầu. |
| 4 | Không có tuổi trong hồ sơ | Không kiểm tra được điều kiện độ tuổi, vì ngày sinh là tuỳ chọn, không kiểm tra, và API không lọc theo trường này. |
| 5 | Không có công cụ nhận giải hay thẩm định | Người thắng không có chỗ nộp giấy, CS không có chỗ duyệt, không có hồ sơ ghi ai thắng dựa trên bằng chứng gì. |
| 6 | Trễ một giờ và huy chương theo chỉ số dòng | Nhỏ nếu đứng riêng, nhưng học sinh đứng nhất lúc 23:30 rồi đứng ba lúc 00:30 sẽ không chấp nhận lời giải thích “view chưa làm mới”. |
| Cấp độ | Lượt làm | Bài khác nhau |
|---|---|---|
| Starters | 822 | 125 |
| Movers | 680 | 602 |
| Flyers | 444 | 353 |
| KET | 849 | 549 |
| PET | 311 | 204 |
Thể lệ ở dạng phụ huynh đọc được. Mục 9 đến 14 tồn tại để các quy tắc này đúng.
| Cấp độ | Học sinh đạt 100 / 100 |
|---|---|
| Starters | 641 |
| Movers | 410 |
| Flyers | 372 |
| KET | 163 |
| PET | 61 |
Cambridge công bố bộ đề Thiếu nhi cho trẻ 6 đến 12 tuổi, nhưng không công bố độ tuổi cố định cho KET và PET for Schools. Khung dưới đây là khung vận hành của FLYER, bám hướng dẫn đó và khối lớp Việt Nam; tuổi tính tròn năm vào ngày cuối tháng trao giải.
| Cấp độ | Hướng dẫn Cambridge | Khung FLYER (tuổi cuối tháng) | Khối lớp VN điển hình | Năm sinh, cho cuộc đua tháng 9/2026 |
|---|---|---|---|---|
| Starters (Pre A1) | YLE, trẻ 6 đến 12; Starters thường 7 đến 8 | 6 đến 8 | Lớp 1 đến 3 | 2018 đến 2020 |
| Movers (A1) | Thường 8 đến 11 | 8 đến 10 | Lớp 3 đến 5 | 2016 đến 2018 |
| Flyers (A2) | Thường 9 đến 12 | 10 đến 12 | Lớp 5 đến 7 | 2014 đến 2016 |
| KET (A2 Key for Schools) | Độ tuổi đi học; thường THCS | 11 đến 14 | Lớp 6 đến 9 | 2012 đến 2015 |
| PET (B1 Preliminary for Schools) | Độ tuổi đi học; thường cuối THCS, THPT | 13 đến 16 | Lớp 8 đến 11 | 2010 đến 2013 |
Nền tảng tính được tuổi từ ngày sinh nhưng không biết ngày đó có thật không, và học sinh có tự làm bài không. Một giáo viên có tên, ký vào mẫu ngắn, trả lời được cả hai và cho CS một số điện thoại để gọi.
| Trường | Mục đích |
|---|---|
| Họ tên học sinh, ngày sinh, trường, khối và lớp | Đối chiếu với hồ sơ; lệch ngày sinh là từ chối tự động. |
| Cấp độ đã thắng, tháng trao giải | Gắn giấy với một chu kỳ để không tái sử dụng được. |
| Xác nhận 1: học sinh nằm trong khung tuổi FLYER của cấp độ này | Điều kiện mà chương trình được xây dựng trên đó. |
| Xác nhận 2: học sinh hiện đang học tiếng Anh ở hoặc quanh cấp độ này | Bắt trường hợp đặt học sinh vào cấp độ thấp để lấy giải nhờ vùng tuổi chồng lấn. |
| Xác nhận 3: theo hiểu biết của giáo viên, học sinh tự mình hoàn thành các bài thi | Biến giáo viên thành người làm chứng, không chỉ là người điền giấy. |
| Họ tên giáo viên, vai trò, trường hoặc trung tâm, số di động, chữ ký, ngày; dấu trường nếu có | Cho CS một người để gọi. Dấu là tuỳ chọn vì giáo viên trung tâm thường không xin được dấu. |
| Họ tên phụ huynh hoặc người giám hộ, số điện thoại, chữ ký, và đồng ý công bố tên, trường và ảnh của người thắng | Cần cho bảng vinh danh và việc giao xe. |
Mẫu là một trang PDF song ngữ, tải từ trang nhận giải. Ảnh chụp trang đã ký bằng điện thoại được chấp nhận; tệp tải lên giới hạn 10 MB và các định dạng JPG, PNG, HEIC, PDF.
Mỗi cấp độ một chu kỳ mỗi tháng, cả năm cấp độ chạy cùng lịch. Bước 1 và 2 là bảng xếp hạng hiện có với công thức mới; bước 3 đến 7 là phần mới.
Thay view tạo tay bằng một view có định nghĩa trong migration, tính điểm theo mục 5.2 và mang số phiên bản in trên bảng.
monthly_prize_ranks (cấp độ, tháng, hồ sơ, điểm, số bài khác nhau, thời gian đã chặn, thời điểm đạt điểm cuối, hạng, phiên bản), làm mới mỗi giờ cạnh learner_test_level_ranks.row_number), không bao giờ là nhóm phần trăm. View cùng họ đang lệch giữa dev và production, không lặp lại ở đây.championship_month và championship_level, ẩn tới khung giờ mở, máy chủ chỉ cho một lượt, mở vào thư viện sau khung giờ. Học thuật bàn giao năm đề trước ngày 20 mỗi tháng.WITH counted AS ( -- mỗi dòng = (học sinh, đề) với lượt tốt nhất trong tháng
SELECT ua.learner_profile_id, ua.test_level_id, ua.check_point_id,
bool_or(cp.championship_month IS NOT NULL) AS is_championship,
max(round(10 * ua.grade / ua.max_grade, 2)) AS best_grade,
min(least(ua.execution_time, tl.execution_time)) AS best_time,
min(ua.submitted_at) AS first_at
FROM user_attempts ua
JOIN test_level_exam_settings tl ON tl.test_level_id = ua.test_level_id
JOIN check_points cp ON cp.id = ua.check_point_id
WHERE ua.type_attempt = 'exam' AND ua.submitted_at IS NOT NULL
AND ua.grade > 0 AND ua.max_grade > 0
AND (ua.submitted_at AT TIME ZONE 'Asia/Ho_Chi_Minh') >= date_trunc('month', now() AT TIME ZONE 'Asia/Ho_Chi_Minh')
GROUP BY 1,2,3
), ranked AS (
SELECT *, row_number() OVER (PARTITION BY learner_profile_id, test_level_id
ORDER BY best_grade DESC, first_at) AS k
FROM counted WHERE NOT is_championship
), best10 AS (
SELECT learner_profile_id, test_level_id,
sum(best_grade) FILTER (WHERE k <= 10) AS score10,
count(*) AS distinct_tests,
sum(best_time) FILTER (WHERE k <= 10) AS counted_time,
max(first_at) FILTER (WHERE k <= 10) AS final_score_at
FROM ranked GROUP BY 1,2
), champ AS ( -- đề vô địch một lượt của tháng
SELECT learner_profile_id, test_level_id, best_grade * 10 AS champ_score, best_time AS champ_time
FROM counted WHERE is_championship
)
SELECT b.learner_profile_id, b.test_level_id,
b.score10 + coalesce(c.champ_score, 0) AS score,
b.score10, c.champ_score, b.distinct_tests, b.counted_time, c.champ_time, b.final_score_at,
row_number() OVER (PARTITION BY b.test_level_id
ORDER BY b.score10 + coalesce(c.champ_score, 0) DESC,
c.champ_time ASC NULLS LAST, b.distinct_tests DESC,
b.counted_time ASC, b.final_score_at ASC) AS rank,
1 AS formula_version
FROM best10 b LEFT JOIN champ c USING (learner_profile_id, test_level_id);
exam, cho cùng thứ hạng trên dev và production.dob_locked_at); đổi phải qua phiếu hỗ trợ kèm giấy xác nhận hoặc giấy khai sinh.Tác vụ hằng đêm, chạy lại lúc chốt, ghi một dòng cho mỗi học sinh và cấp độ: đủ hay không, kèm lý do theo enum cố định để giao diện giải thích được.
| Mã lý do | Ý nghĩa | Hiển thị cho học sinh |
|---|---|---|
ANONYMOUS | Hồ sơ không có tài khoản người dùng | Tạo tài khoản để đủ điều kiện |
NO_PARENT_PHONE | Chưa có số điện thoại phụ huynh đã xác minh | Thêm số điện thoại của phụ huynh |
NO_DOB | Chưa có ngày sinh, hoặc thêm sau ngày 20 | Thêm ngày sinh trước ngày 20 |
AGE_OUT_OF_BAND | Tuổi cuối tháng ngoài khung của cấp độ | Cấp độ này dành cho 8 đến 10 tuổi; có thể đua ở Flyers |
EXCLUDED | Nằm trong danh sách loại trừ (nhân viên, demo, quản trị đối tác, tạm giữ vì tính trung thực) | Không đủ điều kiện nhận giải |
SECONDARY_PROFILE | Người dùng có hồ sơ khác được đánh dấu là chính | Chỉ hồ sơ chính mới có thể thắng |
WON_THIS_YEAR | Đã nhận xe đạp trong năm dương lịch này | Đã thắng vào tháng 3, xin chúc mừng; năm sau thi tiếp |
@flyer.us hoặc @flyer.vn, danh sách học sinh demo bị dời ngày, và hồ sơ thuộc quản trị đối tác.Một trang trên ranking.flyer.us, mở từ thông báo hoặc banner, dành cho người thắng tạm thời và dự bị.
invited, draft, submitted, under_review, verified, rejected, expired, awarded, delivered; mỗi lần đổi đều báo phụ huynh./prize/winners: tên, trường, tỉnh thành, điểm, ảnh trao giải khi được đồng ý./prize/rules hiển thị mục 5 và 6, song ngữ, kèm phiên bản công thức hiện hành.Danh sách chu kỳ trong admin-web, mỗi dòng một tháng và một cấp độ: trạng thái, top 10 đã chốt, và chuỗi người thắng, dự bị 1, dự bị 2 kèm người đang giữ giải.
Danh sách phẳng, lọc theo trạng thái và cấp độ. Mỗi hồ sơ mở một màn hình duy nhất: thẻ học sinh, dòng bảng đã chốt kèm chi tiết mười bài được tính, giấy đã tải lên, số giáo viên, tín hiệu trung thực (mục 13), nhật ký cuộc gọi, nút Duyệt hoặc Từ chối. Chu kỳ chuyển sang đã công bố khi cả năm cấp độ xong, hoặc vào ngày 12.
Danh sách loại trừ kèm lý do, ngày hết hạn và nhật ký kiểm toán. Thao tác ghi đè thủ công để chuyển giải ngoài quy tắc tự động, bắt buộc có ghi chú và hiện trên chu kỳ.
Nếu học sinh thuộc lớp trên FLYER Teacher hoặc Teado.ai, hồ sơ nêu tên giáo viên đó và giáo viên xác nhận ngay trong sản phẩm bằng một lần chạm. Hồ sơ ghi lại tài khoản giáo viên, mạnh hơn một cuộc gọi. Giấy vẫn dùng cho học sinh không có giáo viên trên FLYER.
CSV người thắng theo chu kỳ cho kế toán, và báo cáo tháng về hồ sơ theo kết quả và lý do.
Quy tắc thôi chưa đủ: nền tảng phải đưa tín hiệu để CS xem xét có bằng chứng, và mọi quyết định phải dựng lại được.
| Tín hiệu | Quy tắc | Tác động |
|---|---|---|
| Tốc độ bất thường | Bài được tính đạt 10 điểm hoàn thành dưới 25% thời lượng quy định của cấp độ | Gắn cờ trên hồ sơ; CS hỏi giáo viên cụ thể về điểm này |
| Người dùng nhiều hồ sơ | Tài khoản sở hữu hơn một hồ sơ học sinh có bài làm trong tháng | Chỉ hồ sơ chính đủ điều kiện; gắn cờ để xem xét |
| Chung thiết bị hoặc mạng | Nhiều hồ sơ top 20 cùng cấp độ chung dấu vân thiết bị hoặc cùng mạng | Gắn cờ, không tự động từ chối, vì anh chị em ruột và lớp học có thật |
| Làm bài dồn dập ban đêm | Hơn năm bài được tính nộp trong khoảng 00:00 đến 05:00 trong tháng | Gắn cờ |
| Ngày sinh nhập muộn | Ngày sinh thêm trong vòng mười ngày trước lúc chốt | Tự động NO_DOB cho tháng đó |
| Trùng mẫu đáp án | Hai hồ sơ có chuỗi đáp án giống hệt ở ba bài trở lên | Gắn cờ cả hai; tạm giữ nếu xác nhận |
exam)| Bảng | Cột chính | Ghi chú |
|---|---|---|
prize_cycles | id, month, test_level_id, status, formula_version, frozen_at, announced_at, closed_at | Mỗi cấp độ mỗi tháng một dòng, tạo bởi tác vụ chốt |
prize_standings | cycle_id, learner_profile_id, rank, score, distinct_tests, counted_time, final_score_at, eligible, reasons (jsonb), counted_tests (jsonb) | Ảnh chụp đã chốt của top 50 mỗi cấp độ, cộng dòng của mọi học sinh đủ điều kiện; bất biến |
prize_eligibility | learner_profile_id, test_level_id, month, eligible, reasons (jsonb), computed_at | Hằng đêm; điều khiển nhãn trên bảng |
prize_claims | id, cycle_id, learner_profile_id, position (winner / reserve1 / reserve2), status, letter_file, teacher_name, teacher_role, teacher_org, teacher_phone, parent_name, parent_phone, address, bike_size, consent_publish, reviewer_email, reject_reason, deadline_at, các mốc thời gian | Mỗi học sinh được mời mỗi chu kỳ một dòng |
prize_claim_events | claim_id, actor, action, detail, created_at | Nhật ký chỉ ghi thêm, gồm cả kết quả cuộc gọi |
prize_exclusions | learner_profile_id hoặc user_id, reason, expires_at, created_by | Khởi tạo từ tên miền nhân viên và danh sách học sinh demo |
prize_age_bands | test_level_id, min_age, max_age, effective_from | Học thuật sở hữu; giữ lịch sử |
learner_profiles.dob_locked_at và is_primary, mặc định là hồ sơ lâu nhất có bài làm.date_of_birth và từ chối cập nhật sau khi khoá, trừ vai trò hỗ trợ.monthly_prize_ranks cùng các view hiện có.prize_eligibility cho tháng hiện tại.prize_cycles và prize_standings, tạo hồ sơ nhận giải cho người thắng và hai dự bị mỗi cấp độ, gửi thông báo.monthlyPrizeRanks(testLevelId, month, first, after, filterBy) trả về điểm, số bài khác nhau, hạng, đủ điều kiện và lý do; viewerPrizeStatus(testLevelId) cho danh sách kiểm tra; prizeCycle(month, testLevelId) cho banner và bảng vinh danh.GET /v4/student/prize/claims/current, PUT /v4/student/prize/claims/{id}, POST /v4/student/prize/claims/{id}/letter (multipart), POST /v4/student/prize/claims/{id}/submit.webs-ranking: tab Tháng đọc truy vấn mới, nhãn điều kiện, banner giải thưởng, các trang /prize/claim/[month]/[level], /prize/winners, /prize/rules, và sửa lỗi huy chương theo chỉ số dòng.admin-web: /center/prizes/ gồm cycles, claims, exclusions, age-bands, theo mẫu danh sách phẳng của màn đá tri thức.| Giai đoạn | Nội dung | Kết quả |
|---|---|---|
| Giai đoạn 1, chu kỳ đầu | Trọn bốn yêu cầu A, B, C, D ở mục 9 đến 12, cộng tác vụ chốt và thông báo | Tháng trao giải đầu tiên chạy trên quy tắc phụ huynh đọc được và CS thực thi được |
| Giai đoạn 2 | Xác nhận giáo viên trong sản phẩm, tín hiệu trung thực, so trùng mẫu đáp án, dự bị nộp trước, xuất dữ liệu kế toán | Thẩm định mất vài giờ thay vì vài ngày, không còn dựa vào gọi điện |
| Giai đoạn 3 | Áp công thức có phiên bản cho Tuần và Năm, giải phụ theo trường và tỉnh, giải tuần bằng đá tri thức | Một họ công thức trên mọi tab |
| Ngoài phạm vi | Giải tiền mặt, giải cho cấp độ IELTS hoặc TOEFL, thay đổi tab Ngày, thay đổi cửa hàng đá tri thức |
| Khoản | Giả định | VND mỗi tháng |
|---|---|---|
| Xe đạp | 5 × 2.500.000 đến 3.500.000 (cỡ trẻ em và thiếu niên) | 12.500.000 đến 17.500.000 |
| Giao hàng | 5 × 150.000 đến 400.000, toàn quốc | 750.000 đến 2.000.000 |
| Thời gian CS | 15 hồ sơ gồm dự bị × 30 phút | khoảng 1 ngày công |
| Tổng | khoảng 14.000.000 đến 20.000.000 |
Trẻ sáu tuổi và học sinh mười sáu tuổi không đi cùng một cỡ xe. Mẫu nhận giải hỏi cỡ, mua sắm giữ hai mẫu.
| Câu hỏi | Vì sao ảnh hưởng tới thiết kế | Người quyết |
|---|---|---|
| Xác nhận khung tuổi ở mục 6, đặc biệt KET 11 đến 14 và PET 13 đến 16. | Cambridge không công bố độ tuổi cố định cho bài for Schools, nên đây là lựa chọn của FLYER và quyết định ai được thắng. | Học thuật |
| Có nhận học sinh nhỏ hơn khung không? | Trẻ bảy tuổi thắng Flyers tốt cho marketing, nhưng loại trừ thì dễ giải thích hơn. Bản nháp mặc định loại trừ. | Học thuật + Sản phẩm |
| Mười bài, hay mức chặn khác? | Mười bài là bốn đến năm giờ ở Starters; nếu quá nhiều với trẻ sáu tuổi thì Starters dùng tám. | Học thuật |
| Học thuật có soạn được năm đề đầy đủ hoàn toàn mới, chưa công bố, mỗi tháng không? | Đề vô địch chỉ có giá trị khi chưa ai từng thấy. Nếu thiếu năng lực, rút ngẫu nhiên từ kho niêm phong hai mươi đề mỗi cấp độ. | Học thuật + Nội dung |
| Khung giờ đề vô địch: cả cuối tuần cuối tháng, hay chỉ Chủ nhật? | Khung dài công bằng hơn với gia đình bận cuối tuần, khung ngắn thì khó chia sẻ đáp án hơn. Bản nháp dùng 08:00 thứ Bảy đến 22:00 Chủ nhật. | Sản phẩm + Học thuật |
| Công thức có phiên bản có thay luôn Ngày, Tuần, Năm từ đầu không? | Gọn hơn nhưng đổi các con số học sinh đang theo dõi và mở rộng giai đoạn 1. Bản nháp chỉ áp cho tab Tháng. | Sản phẩm |
| Số điện thoại phụ huynh có sẵn trên đủ nhiều hồ sơ không? | Nếu phần lớn hồ sơ thiếu, bắt buộc ngay tháng đầu sẽ loại gần hết top 10. | Sản phẩm + Dữ liệu |
| Ai ký cho học sinh học tại nhà hoặc không có giáo viên tiếng Anh? | Bản nháp chấp nhận giáo viên trung tâm và chủ nhiệm lớp 1 đến 3, nhưng chưa có lời giải cho trẻ không có cả hai. | CS + Học thuật |
| Công bố công khai chi tiết điểm của người thắng? | Minh bạch giảm tranh chấp nhưng lộ việc trẻ đã làm đề nào. Bản nháp chỉ hiện chi tiết cho người nhận giải và quản trị. | Sản phẩm + Pháp lý |
| Duyệt ngân sách và mua hai mẫu xe. | Khoảng 14 đến 20 triệu đồng mỗi tháng chưa gồm marketing. | Kế toán |
flyer/webs-ranking (src/pages/index.tsx, src/components/TableRanking, src/api/common.ts, src/providers/auth), flyer/exam (graph/schema/type_extended.graphql, graph/user/type_extended.resolvers.go, ent/schema/views.go, module/mview, module/learnerprofile) và flyer/flyer-go (db/migrations/base/0092_user_score_views.up.sql, service/student/db/queries/score.sql). Định nghĩa thật trên production của learner_test_level_ranks phải đọc từ pg_matviews; nó không nằm trong kho mã nào.