Skip to content

SDD-049 - Chương trình trường học tháng 10.2026 ​

Nguồn: SRC-1110 (chủ dự án 28.09.2026). Trang ưu đãi công khai là việc của SRC-1109 (nemo12.com/promo/schools-october-2026/<lang>, SDD-029); tài liệu này lo phần sau trang ấy. Thay ưu đãi cũ của SRC-1057 (100 suất IELTS cho trường cấp 3 Việt Nam có người đại diện).

1. Ưu đãi, và những gì hệ thống phải giữ ​

Mỗi trường cấp 3 tham gia nhận 100 suất NEMO IELTS + 100 suất NEMO SAT, miễn phí 100%, là một chương trình khuyến mại để trường và học sinh làm quen hai chương trình. Chỉ trong tháng 10.2026 (01.10.2026 - 31.10.2026, giờ Việt Nam). Trường tham gia bằng cách trả lời thư (hoặc viết tới hello@nemo12.com); sau khi xác nhận, Nemo12 gửi trường một mã 200 lượt, học sinh nhập mã khi đăng ký.

Giao diện học cho học sinh ngoài Việt Nam là tiếng Anh; thư mời nói rõ điều đó, không hứa bản địa hoá sản phẩm.

2. Mã trường (REQ-GRW-04) ​

Module workers/api/src/modules/schoolCodes/, bảng ở migration 0308.

BảngVai trò
school_codesMột dòng một mã: tên trường, ielts_seats (100), sat_seats (100), valid_from/valid_to (ngày lịch VN), access_days (90), status active/revoked
school_code_redemptionsMột dòng một suất đã cấp: mã, chương trình, learner, ngày hết suất. Duy nhất (program, learner_id)
learner_sat_membershipGói SAT của learner, cùng hình dạng learner_ielts_membership (0286); SAT chưa có bảng gói nào trước đó

Suất đi vào mô hình gói đã có. IELTS: learner_ielts_membership lên plan member, ends_on kéo tới ngày hết suất (không bao giờ rút ngắn một kỳ dài hơn đã có). SAT: learner_sat_membership. Nhờ vậy trang Membership, chip "Free: n days left" và thư nhắc kỳ dùng thử (chỉ đọc plan trial) tự đổi theo mà không phải sửa chỗ nào.

Idempotent và không tràn suất, cả hai chốt nằm ở D1:

  • Chỉ mục duy nhất (program, learner_id): nhập lại cùng mã trả already: true, nhập mã của trường thứ hai cho cùng chương trình bị từ chối already_redeemed.
  • Câu INSERT ... SELECT ... WHERE (đếm suất đã dùng) < (trần) đếm và ghi trong một câu lệnh, nên hai học sinh bấm cùng lúc lúc còn một suất không thể cùng lọt (có test).

Lời từ chối hai thứ tiếng (REFUSALS trong service.ts): invalid 404, revoked 410, not_started 409, expired 410, full 409, already_redeemed 409. Mỗi lỗi mang message (tiếng Anh, cho sân IELTS - SRC-834) và message_vi. Một chương trình được, một chương trình hết suất thì vẫn 200 và từng dòng results nói bên nào vì sao.

RouteAiViệc
GET /v1/public/school-codes/{code}công khaiTên trường, hạn, suất còn lại mỗi chương trình (màn đăng ký, chưa có phiên)
GET/POST /v1/learners/{learnerId}/school-codelearner, bố mẹ (canAccessLearner)Đọc suất đang có / nhập mã
GET/POST /v1/admin/school-codesadminLiệt kê kèm số suất / tạo mã (NEMO-XXXXXX, bảng chữ không có 0/O/1/I/L)
GET /v1/admin/school-codes/{code}/redemptionsadminLượt nhập
POST /v1/admin/school-codes/{code}/revokeadminThu hồi: không nhập thêm được, suất đã cấp giữ nguyên

Học sinh nhập mã ở hai chỗ (learn):

  1. Màn đăng nhập của sân IELTS (ielts/SignInGate.tsx): ô "School code (optional)", hoặc link có ?school_code=. Gõ xong thấy ngay tên trường và số suất còn lại. Mã giữ trong localStorage và AuthProvider nhập hộ ngay khi biết learner (ielts/schoolCodeClaim.ts).
  2. Trang /ielts/membership: thẻ "School code" (tiếng Anh đơn giản, SRC-1019), liệt kê suất đang có và ô nhập mã.

3. Sổ outreach (REQ-GRW-05) ​

Module workers/api/src/modules/outreach/, tab Outreach ở admin.nemo12.com (apps/admin/src/pages/Outreach.tsx).

BảngVai trò
outreach_orgsTrường, hiệp hội, bộ/sở giáo dục, hội đồng: country, language, type, name_local/en, city, website, email chính thức, facebook_url, linkedin_url, source_urls (JSON), confidence, status, approved, school_code
outreach_attemptsMỗi lượt liên hệ: channel email/facebook_inbox/linkedin, kind initial/reminder, template, ngôn ngữ, sent_at, sent_by, status, message id, lỗi, vn_date
outreach_status_logLịch sử trạng thái + ghi chú (replied, interested, joined, declined, opted_out, bounced)
outreach_blocksDanh sách chặn: email hoặc cả tên miền
outreach_templatesKhuôn thư theo ngôn ngữ và loại, là dữ liệu (sửa không cần deploy), có cờ duyệt

Nạp CSV country,name_local,name_en,city,type,website,email,facebook_url,linkedin_url,language,confidence,source_urls,notes (csv.ts, bộ đọc RFC 4180 tự viết). Trùng email (hoặc trùng country + name_local khi không có email) thì cập nhật liên hệ, không đụng status/approved. Dòng sai (mã quốc gia, email) báo lại kèm số dòng. Chỉ nhận email chính thức công khai của trường, không email cá nhân giáo viên - đây là luật cho người soạn danh sách, cột source_urls là chỗ chứng minh.

Facebook: Page không mở được hội thoại với Page khác, nên chủ dự án inbox từ trang cá nhân, tối đa 20/ngày. Màn admin có nút "Đã inbox FB" ghi một lượt logged, và bộ đếm "x/20 hôm nay". Vượt 20 vẫn ghi (một tin đã gửi thật không được mất khỏi sổ) nhưng bộ đếm chuyển màu và API trả over_limit.

Đặt trạng thái opted_out hoặc bounced tự đưa email vào danh sách chặn.

4. Gửi thư mời (REQ-GRW-05) ​

Kênh: dùng lại Cloudflare Email Service qua binding EMAIL (SRC-602), hàm mới sendOutreachEmail trong email/channel.ts. Khác sendEmail ở hai chỗ: đi từ Nemo12 <hello@nemo12.com> với replyTo: hello@nemo12.com và không lùi về no-reply@ khi bị từ chối (thư mời mà trả lời vào hộp không ai đọc thì thà không gửi); bản đối chứng về EMAIL_COPY_TO luôn là CC (nhìn thấy được), không theo công tắc EMAIL_COPY_MODE: chỉ đạo chủ dự án 28.09.2026 (SRC-1116) là mọi thư đi từ hello@nemo12.com phải CC dac2205@gmail.com để chủ dự án theo dõi, thay cho BCC ban đầu. Đánh đổi đã được chủ dự án chấp nhận: trường nhận thư NHÌN THẤY hộp thư cá nhân ấy trong danh sách người nhận, và "Trả lời tất cả" gửi thẳng vào đó. Địa chỉ chỉ đến từ một giá trị cấu hình (EMAIL_COPY_TO), không mã cứng. Chốt xem trước DIGEST_TEST_RECIPIENT vẫn có hiệu lực: còn đặt thì thư về đó, tiêu đề mang [TEST -> địa chỉ thật], và lượt ấy không tiêu suất của trường.

Khuôn thư vi, en, id, th cho thư đầu và thư nhắc, seed ở migration 0308 với approved = 0. Chỗ điền: {{greeting}} (câu chào của khuôn, hoặc contact_greeting riêng của tổ chức), {{school_name}}, {{country}} (tên nước theo ngôn ngữ thư, Intl.DisplayNames), {{promo_url}} (mỗi khuôn một link, mặc định trang của SRC-1109 https://nemo12.com/promo/schools-october-2026/<lang>). Dòng "reply 'unsubscribe'" do MÃ gắn theo ngôn ngữ (OPT_OUT trong sender.ts), khuôn không gỡ được. Sửa chữ một khuôn đã duyệt thì khuôn tự bỏ duyệt. Ngôn ngữ chưa có khuôn dùng khuôn tiếng Anh.

Luật gửi, tất cả trong runBatch (outreach/sender.ts); xem trước và gửi thật đi cùng một đường mã, dừng khác chỗ:

LuậtThi hành
Chỉ gửi dòng đã duyệtoutreach_orgs.approved = 1 (chủ dự án duyệt 10 thư đầu bằng đúng cờ này) và khuôn của ngôn ngữ ấy approved = 1
1 thư đầu + 1 thư nhắc mỗi tổ chứcChỉ mục duy nhất (org_id, kind) WHERE channel='email' AND status IN ('sending','sent'); giành chỗ trước, gửi sau
Thư nhắcChỉ khi thư đầu đã đi ít nhất 5 ngày và trạng thái vẫn contacted (đã trả lời thì thôi)
50 thư/ngàyĐếm outreach_attempts theo vn_date (ngày lịch VN)
Danh sách chặnoutreach_blocks (email hoặc tên miền, cả tên miền con) và email_suppressions
Không gửi sau 31.10.2026LAST_SEND_DAY
Công tắcOUTREACH_SEND_ENABLED phải đúng "1"; mặc định "0" trong wrangler.jsonc
Không bấm nhầmPOST /v1/admin/outreach/send đòi confirm: true; màn admin hỏi lại một lần

Gửi hỏng thì dòng failed không giữ chỗ, lượt sau gửi lại được.

Route (admin)Việc
GET /v1/admin/outreach/summaryBộ đếm email/50, Facebook/20, theo trạng thái, theo nước, công tắc
GET /v1/admin/outreach/orgs?country=&status=&q=Danh sách lọc
POST /v1/admin/outreach/importNạp CSV
GET/PATCH /v1/admin/outreach/orgs/{id}Chi tiết + lịch sử / trạng thái, duyệt, ghi chú, câu chào
POST /v1/admin/outreach/orgs/{id}/attemptsGhi tay inbox Facebook / LinkedIn
GET/PATCH /v1/admin/outreach/templates[/{id}]Khuôn thư, duyệt
GET/POST/DELETE /v1/admin/outreach/blocksDanh sách chặn
POST /v1/admin/outreach/previewChạy khô: ai nhận thư nào, thư đã điền; không gửi, không ghi
POST /v1/admin/outreach/sendGửi lô đã duyệt

5. hello@nemo12.com: đã kiểm gì, chủ dự án phải làm gì ​

Kiểm DNS ngày 28.09.2026 (chỉ đọc, dig):

Bản ghiHiện trạng
MX nemo12.comKhông có. Thư gửi tới hello@nemo12.com hiện không có chỗ nhận, trường trả lời sẽ bị trả về
TXT nemo12.com (SPF)Không có
TXT _dmarc.nemo12.comv=DMARC1; p=reject;
MX/TXT cf-bounce.nemo12.comMX route1..3.mx.cloudflare.net, SPF include:_spf.mx.cloudflare.net - đây là tên miền gửi của Email Service (SRC-602), không nhận thư

Repo không có Worker email() nào và không có tài liệu nào về Email Routing cho tên miền gốc. Vì vậy công tắc gửi để "0". Trước khi bật, chủ dự án làm trên Cloudflare dashboard:

  1. Email Routing cho nemo12.com (Email > Email Routing > Enable). Cloudflare tự thêm ba bản ghi MX route1/2/3.mx.cloudflare.net và một TXT SPF v=spf1 include:_spf.mx.cloudflare.net ~all ở tên miền gốc. Kiểm lại không có MX cũ nào khác.
  2. Quy tắc: hello@nemo12.com -> dac2205@gmail.com (địa chỉ đích đã Verified từ SRC-602). Không bật catch-all.
  3. Gửi thử một lá tay từ một hộp thư ngoài tới hello@nemo12.com, xác nhận nó tới Gmail.
  4. Gửi đi từ hello@: Email Service xác minh theo TÊN MIỀN, và nemo12.com đã onboard, nên hello@ về nguyên tắc gửi được như ielts@/sat@ (SRC-1091). Thư no-reply@ đã tới hộp thư phụ huynh thật dưới DMARC p=reject đang có, nên chiều gửi của tên miền đã qua DMARC; phiên này KHÔNG kiểm được bản ghi DKIM (selector nằm trong dashboard), nên đừng coi đây là đã xác minh. Hai việc phải kiểm trước lượt gửi thật: (a) nạp một dòng thử mang email của chính chủ dự án, duyệt, gửi lô chỉ có dòng ấy, rồi mở "Show original" trong Gmail, thấy SPF: PASS, DKIM: PASS, DMARC: PASS; (b) Email Service còn giới hạn "chỉ gửi tới địa chỉ đã xác minh" của bản beta không (SDD-005 §5.1). Nếu còn, thư tới trường bị E_RECIPIENT_NOT_ALLOWED, lượt ấy ghi failed, không ai nhận.
  5. Muốn trả lời trường TỪ hello@ (chứ không từ Gmail cá nhân): thêm hello@nemo12.com làm "Send mail as" trong Gmail qua một SMTP gửi được (Email Routing chỉ chuyển tiếp, không gửi). Không làm thì trả lời sẽ đi từ dac2205@gmail.com.
  6. Làm xong 1-3: đổi "OUTREACH_SEND_ENABLED": "1" trong workers/api/wrangler.jsonc, commit, CI deploy.

Trả lời "unsubscribe" hiện được xử lý TAY: người đọc hộp thư đặt trạng thái opted_out ở tab Outreach, email vào danh sách chặn. Tự động hoá (Worker email() đọc chữ "unsubscribe") là việc sau, cần Email Routing trước.

Cập nhật 28.09.2026: bước 1-2 đã xong trên dashboard. Kiểm lại bằng dig @1.1.1.1: MX route1/2/3.mx.cloudflare.net, TXT v=spf1 include:_spf.mx.cloudflare.net ~all, DKIM cf2024-1._domainkey. Quy tắc hello@nemo12.com -> dac2205@gmail.com Active, catch-all tắt. Công tắc chuyển "1". Bước 3-4 (thư thử tới hello@, lượt gửi thử một dòng về chính chủ dự án) vẫn phải làm trước lô thật.

6. Câu hỏi còn mở ​

  • Q-193: một suất miễn phí dùng được bao lâu? Tạm 90 ngày từ ngày nhập mã (access_days, sửa được từng mã).
  • Q-194: khu SAT hiện không chặn ai theo gói, nên learner_sat_membership mới là sổ ghi, chưa là cổng.
  • Q-195 (✅): link trang ưu đãi là trang của SRC-1109, sửa được ở admin.

Trace ​

REQSection
REQ-GRW-04§2
REQ-GRW-05§3, §4, §5