REFORMATION BRIEF R-03A — THE FIRST PACK¶
Generic Pack Generation + Delivery Pipeline¶
UCCA Platform · 18 March 2026¶
R-00 built the rails. R-01 opened the door. R-02 put the till on the counter. R-03A puts the first product on the shelf. This brief builds the generic pack generation and delivery pipeline. No contextualisation. No Mavis. No AI video. Just: unit in, pack out, entitlement delivered.
SURFACE: ucca-api (engine/ucca-engine/backend/) + rtopacks-site (rtopacks.com.au)
DO NOT TOUCH: ucca-site, ucca-ir, ucca-keys, ucca-track, ucca-ops
Cloudflare Account: e5a9830215a8d88961dc6c80a8c7442a
engine-db: 0efa8970-0053-4623-8436-4e877af10887
rtopacks-db: 334ac8fb (TGA corpus — 15,202 units, 8,007 quals)
R2 bucket: ucca-ir-assets (use for pack storage, create packs/ prefix)
→ ALEX¶
1. What a Generic Pack Is¶
A generic pack for one unit of competency contains:
- Assessment Tool — performance evidence checklist mapped to every PE in the unit
- Knowledge Evidence Questions — question bank mapped to every KE in the unit
- Mapping Matrix — shows which assessment task covers which PE/KE/AC
- Assessment Conditions Summary — pulled directly from the unit's Assessment Conditions
- Cover Sheet — unit code, title, version, RTO name (from tenant), date generated
All five documents generated programmatically from TGA data already in rtopacks-db. No AI inference for Tier 1 — deterministic generation from structured data. The unit tells us what's required. We produce it.
Output formats: PDF + DOCX for each document. Zipped into a single .zip file.
2. Schema — engine-db¶
Add packs table:
CREATE TABLE IF NOT EXISTS packs (
id TEXT PRIMARY KEY,
tenant_id TEXT REFERENCES tenants(id),
purchase_id TEXT REFERENCES purchases(id),
unit_code TEXT NOT NULL,
qual_code TEXT, -- NULL for single unit packs
pack_type TEXT NOT NULL DEFAULT 'generic' CHECK (pack_type IN ('generic','contextualised')),
status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued','generating','ready','failed')),
r2_key TEXT, -- path in R2 when ready
download_url TEXT, -- signed URL, refreshed on request
download_expires INTEGER, -- unix timestamp
manifest_json TEXT NOT NULL DEFAULT '{}', -- what's in the pack
generated_at INTEGER,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
error_text TEXT -- if status = failed
);
CREATE INDEX IF NOT EXISTS idx_packs_tenant ON packs(tenant_id);
CREATE INDEX IF NOT EXISTS idx_packs_purchase ON packs(purchase_id);
CREATE INDEX IF NOT EXISTS idx_packs_unit ON packs(unit_code);
CREATE INDEX IF NOT EXISTS idx_packs_status ON packs(status);
3. Pack Generation Pipeline — ucca-api¶
Add backend/app/routers/packs.py.
Endpoint: POST /v1/packs/generate¶
Triggered automatically on successful purchase (webhook calls this after writing to purchases table).
# POST /v1/packs/generate
# Body: { purchase_id, unit_code, tenant_id, qual_code (optional) }
# Auth: internal only — called from webhook handler, not public
async def generate_pack(body: GeneratePackRequest):
# 1. Create pack record in queued state
pack_id = uuid4()
db.insert("packs", {
"id": pack_id,
"tenant_id": body.tenant_id,
"purchase_id": body.purchase_id,
"unit_code": body.unit_code,
"qual_code": body.qual_code,
"pack_type": "generic",
"status": "queued"
})
# 2. Pull unit data from rtopacks-db
unit = rtopacks_db.get_unit(body.unit_code)
# unit contains: code, title, version, elements, performance_criteria,
# knowledge_evidence, performance_evidence, assessment_conditions
# 3. Pull tenant data for cover sheet
tenant = db.get_tenant(body.tenant_id)
# 4. Generate documents
db.update("packs", pack_id, {"status": "generating"})
try:
documents = generate_all_documents(unit, tenant)
# Returns: { assessment_tool, knowledge_questions, mapping_matrix,
# assessment_conditions, cover_sheet }
# Each is: { pdf: bytes, docx: bytes }
# 5. Zip and upload to R2
zip_bytes = create_zip(documents, unit)
r2_key = f"packs/{pack_id}/{unit.code}_generic_pack.zip"
r2.put(r2_key, zip_bytes)
# 6. Generate signed download URL (7 day expiry)
download_url = r2.get_signed_url(r2_key, expires=604800)
# 7. Update pack record
db.update("packs", pack_id, {
"status": "ready",
"r2_key": r2_key,
"download_url": download_url,
"download_expires": int(time.time()) + 604800,
"manifest_json": json.dumps({
"unit_code": unit.code,
"unit_title": unit.title,
"unit_version": unit.version,
"documents": [
"Assessment Tool (PDF + DOCX)",
"Knowledge Evidence Questions (PDF + DOCX)",
"Mapping Matrix (PDF + DOCX)",
"Assessment Conditions Summary (PDF + DOCX)",
"Cover Sheet (PDF + DOCX)"
],
"generated_at": int(time.time())
}),
"generated_at": int(time.time())
})
# 8. Update purchase delivery status
db.update("purchases", body.purchase_id, {"delivery_status": "delivered"})
# 9. Write audit log
write_audit(ctx, "pack.generated", "packs", pack_id, {"unit_code": unit.code}, request)
# 10. Send notification
notify_pack_ready(tenant, unit, download_url)
except Exception as e:
db.update("packs", pack_id, {"status": "failed", "error_text": str(e)})
write_audit(ctx, "pack.failed", "packs", pack_id, {"error": str(e)}, request)
4. Document Generation Functions¶
Add backend/app/services/pack_generator.py.
All generation is deterministic — no AI, no inference. Direct from TGA structured data.
Assessment Tool¶
def generate_assessment_tool(unit: Unit, tenant: Tenant) -> Document:
"""
Performance evidence checklist.
One row per performance criterion.
Columns: Element | PC | Evidence requirement | Satisfactory | Comments
"""
doc = Document()
# Header
doc.add_heading(f"Assessment Tool — {unit.code}", 0)
doc.add_paragraph(f"Unit: {unit.title}")
doc.add_paragraph(f"Version: {unit.version}")
doc.add_paragraph(f"RTO: {tenant.display_name}")
doc.add_paragraph(f"Generated: {date.today()}")
# Table: one row per PC across all elements
table = doc.add_table(rows=1, cols=5)
headers = ["Element", "Performance Criterion", "Evidence Required", "S/NYS", "Comments"]
for i, h in enumerate(headers):
table.rows[0].cells[i].text = h
for element in unit.elements:
for pc in element.performance_criteria:
row = table.add_row()
row.cells[0].text = element.number
row.cells[1].text = pc.text
row.cells[2].text = pc.evidence_requirement or "Direct observation"
row.cells[3].text = "□ S □ NYS"
row.cells[4].text = ""
return doc
Knowledge Evidence Questions¶
def generate_knowledge_questions(unit: Unit, tenant: Tenant) -> Document:
"""
One question per knowledge evidence item.
Questions are restatements of the KE as interrogatives.
"""
doc = Document()
doc.add_heading(f"Knowledge Evidence Questions — {unit.code}", 0)
for i, ke in enumerate(unit.knowledge_evidence, 1):
doc.add_paragraph(f"Q{i}. {ke_to_question(ke.text)}", style='List Number')
doc.add_paragraph("Answer:", style='Normal')
doc.add_paragraph("_" * 80) # answer line
return doc
def ke_to_question(ke_text: str) -> str:
"""Convert KE statement to question format."""
# "knowledge of workplace safety procedures"
# → "Describe the workplace safety procedures relevant to this role."
# Simple rule-based transformation — no AI needed
if ke_text.lower().startswith("knowledge of"):
return f"Describe {ke_text[len('knowledge of'):].strip()}."
elif ke_text.lower().startswith("understanding of"):
return f"Explain {ke_text[len('understanding of'):].strip()}."
else:
return f"Describe your knowledge of {ke_text.strip()}."
Mapping Matrix¶
def generate_mapping_matrix(unit: Unit) -> Document:
"""
Shows which assessment task covers which PE/KE/AC.
Rows: assessment tasks (observation, questions, third party)
Columns: each PE and KE
"""
doc = Document()
doc.add_heading(f"Mapping Matrix — {unit.code}", 0)
# Build column headers from all PCs and KEs
all_criteria = []
for element in unit.elements:
for pc in element.performance_criteria:
all_criteria.append(f"{element.number}.{pc.number}")
for i, ke in enumerate(unit.knowledge_evidence, 1):
all_criteria.append(f"KE{i}")
# Assessment tasks (standard for generic pack)
tasks = [
"Direct observation",
"Knowledge questions",
"Third party report",
"Portfolio evidence"
]
table = doc.add_table(rows=len(tasks)+1, cols=len(all_criteria)+1)
table.rows[0].cells[0].text = "Assessment Task"
for i, c in enumerate(all_criteria):
table.rows[0].cells[i+1].text = c
for j, task in enumerate(tasks):
table.rows[j+1].cells[0].text = task
# Mark coverage — observation covers PCs, questions cover KEs
for i, c in enumerate(all_criteria):
if task == "Direct observation" and not c.startswith("KE"):
table.rows[j+1].cells[i+1].text = "✓"
elif task == "Knowledge questions" and c.startswith("KE"):
table.rows[j+1].cells[i+1].text = "✓"
return doc
Assessment Conditions Summary¶
def generate_assessment_conditions(unit: Unit, tenant: Tenant) -> Document:
"""
Direct pull from unit assessment conditions.
No transformation — verbatim from TGA.
"""
doc = Document()
doc.add_heading(f"Assessment Conditions — {unit.code}", 0)
doc.add_paragraph("The following assessment conditions apply to this unit:")
doc.add_paragraph(unit.assessment_conditions)
doc.add_paragraph(
"Note: These conditions are prescribed by the training package and "
"cannot be modified. RTOs must ensure all conditions are met before "
"conducting assessment."
)
return doc
Cover Sheet¶
def generate_cover_sheet(unit: Unit, tenant: Tenant, pack_id: str) -> Document:
doc = Document()
doc.add_heading("Assessment Pack", 0)
details = [
("Unit Code", unit.code),
("Unit Title", unit.title),
("Training Package", unit.training_package),
("Version", unit.version),
("RTO", tenant.display_name),
("RTO Number", tenant.config_json.get("rto_number", "")),
("Pack Type", "Generic — suitable for contextualisation"),
("Generated", date.today().strftime("%d %B %Y")),
("Pack ID", str(pack_id)),
("Verify", f"keys.ucca.online/verify/{pack_id[:8]}"),
]
table = doc.add_table(rows=len(details), cols=2)
for i, (label, value) in enumerate(details):
table.rows[i].cells[0].text = label
table.rows[i].cells[1].text = str(value)
doc.add_paragraph(
"\nThis pack was generated by RTOpacks — "
"rtopacks.com.au\n"
"United Central Colleges of Australia Pty Ltd · ABN 59 168 872 535\n"
"PO Box 5377, Stafford Heights QLD 4053"
)
return doc
5. PDF Generation¶
Use weasyprint to convert DOCX → HTML → PDF, or reportlab for direct PDF generation.
Add to requirements.txt:
def docx_to_pdf(doc: Document) -> bytes:
"""Convert python-docx Document to PDF bytes."""
# Save docx to temp file
with tempfile.NamedTemporaryFile(suffix='.docx', delete=False) as tmp:
doc.save(tmp.name)
docx_path = tmp.name
# Convert to PDF via weasyprint
# (or use subprocess with LibreOffice if available)
pdf_bytes = convert_docx_to_pdf(docx_path)
os.unlink(docx_path)
return pdf_bytes
6. Notification — Pack Ready¶
On pack ready, notify the buyer via email and SMS.
def notify_pack_ready(tenant: Tenant, unit: Unit, download_url: str):
# Email via Auth0 (or stub — just log for now)
# SMS via Twilio
twilio_client.messages.create(
body=f"Your RTOpacks pack for {unit.code} {unit.title} is ready. "
f"Log in to download: rtopacks.com.au/dashboard",
from_=TWILIO_PHONE_NUMBER,
to=tenant.config_json.get("mobile", "")
)
7. Download Endpoint¶
# GET /v1/packs/{pack_id}/download
# Auth: L3 or L4 JWT, must own the pack
async def download_pack(pack_id: str, ctx: AuthContext = Depends(get_current_user)):
pack = db.get_pack(pack_id)
# Verify ownership
assert pack.tenant_id == ctx.tenant_id or ctx.role in ("L1", "L2")
# Refresh signed URL if expired
if pack.download_expires < int(time.time()):
new_url = r2.get_signed_url(pack.r2_key, expires=604800)
db.update("packs", pack_id, {
"download_url": new_url,
"download_expires": int(time.time()) + 604800
})
pack.download_url = new_url
write_audit(ctx, "pack.downloaded", "packs", pack_id, {}, request)
return {"download_url": pack.download_url}
8. Dashboard — Card 3 Update¶
Update Course Packs card on L3 dashboard to show purchased packs:
┌─────────────────────────────────────────┐
│ 📦 Course Packs │
│ │
│ AURTTA2014 — Inspect and service │
│ cooling systems │
│ Generic pack · Generated 18 Mar 2026 │
│ [ Download ] [ View details ] │
│ │
│ [ Browse catalogue → ] │
└─────────────────────────────────────────┘
9. Product Catalogue — rtopacks.com.au¶
Add a simple product listing to the catalogue. For now — stub with 5 representative units across different training packages. Real units from rtopacks-db.
Pick one unit from each: - Automotive (AUR) - Aged Care (CHC) - Hospitality (SIT) - Construction (CPC) - Business (BSB)
Each listing shows:
- Unit code + title
- Training package
- What's included (5 documents)
- Price: $29.00 (inc. GST)
- [ Add to cart ] button → triggers checkout-auth gate → Stripe checkout
Pricing rule for generic packs: - Single unit: $29.00 inc. GST - Full qualification (core + minimum electives): calculate at checkout based on unit count × $19.00 (bulk discount) - Extra electives: $19.00 per unit added to qual bundle
10. Stripe Products — Create in Dashboard¶
Tim to create in Stripe RTOPacks.com.au account:
- Product: "RTOpacks Generic Unit Pack"
- Price: $29.00 AUD one-time
-
Price ID: copy and add to wrangler.toml as
GENERIC_UNIT_PRICE_ID -
Product: "RTOpacks Qualification Bundle"
- Price: calculated at checkout (variable)
- Use Stripe's custom amount feature
OPS STUB TASK¶
Add to ops console under packs world (create /w/packs stub):
R-03A · THE FIRST PACK
packs table in engine-db ✓
Pack generation pipeline live ✓
R2 storage wired (packs/ prefix) ✓
PDF + DOCX generation live ✓
Download endpoint live ✓
Dashboard Card 3 updated ✓
5 catalogue listings live ✓
SMS notification on pack ready ✓
→ TIM¶
What this brief does:
R-03A puts the first product on the shelf. When an RTO buys a unit pack, the engine pulls the unit from the TGA corpus, generates five documents — assessment tool, knowledge questions, mapping matrix, assessment conditions, and a cover sheet — zips them into a download, stores them in R2, and sends the RTO an SMS saying their pack is ready. They log into their dashboard, click download, and they have a compliant, mapped assessment pack for that unit.
No AI inference. No contextualisation. No Mavis. Just deterministic generation from TGA data. Fast, cheap to run, and more accurately mapped than anything the content farms produce — because it's generated directly from the unit structure, not written by someone who read the unit once.
What you need to do:
Go to Stripe → RTOPacks.com.au → Product catalog → Add product:
- Name: RTOpacks Generic Unit Pack
- Price: $29.00 AUD, one-time
- Save and copy the Price ID (starts with price_...)
- Give the Price ID to Alex
What this is NOT: - Not contextualised — that's R-03B - Not the compliance checker — that's R-03B - Not the Content Studio — that's R-03B - Not beautiful documents — functional, correctly mapped, ready to use - Not AI generated content — deterministic from TGA data
The test: An RTO buys AURTTA2014. Gets an SMS. Logs in. Downloads a zip. Opens the assessment tool. Every performance criterion from the unit is in there, mapped, with evidence columns. Opens the knowledge questions. Every knowledge evidence item has a question. Opens the mapping matrix. It all maps. Takes it to an ASQA auditor. Auditor nods.
That's the first dollar. That's the coin in the jukebox.
REFORMATION BRIEF R-03A · UCCA Inc · 18 March 2026 "Unit in. Pack out. First dollar."