> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sunschool.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Points System

> How learners earn, track, and spend points in Sunschool

## Overview

Sunschool uses a comprehensive points economy to motivate and reward learners. Points are earned through learning activities and can be saved toward parent-defined rewards.

## Point Sources

Learners earn points from multiple sources:

```typescript theme={null}
// From server/services/points-service.ts:3
export type PointsSource =
  | "QUIZ_CORRECT"      // Earned for correct quiz answers
  | "LESSON_COMPLETE"   // Earned for completing lessons
  | "ACHIEVEMENT"       // Bonus points from achievements
  | "REDEMPTION"        // Points deducted for rewards
  | "ADMIN_ADJUST";     // Manual parent/admin adjustments
```

<CardGroup cols={3}>
  <Card title="Quiz Questions" icon="circle-check">
    Points per correct answer
  </Card>

  <Card title="Lesson Completion" icon="book-open">
    Bonus for finishing lessons
  </Card>

  <Card title="Achievements" icon="trophy">
    Special milestone rewards
  </Card>
</CardGroup>

## Points Ledger

Every point transaction is recorded in a permanent ledger:

```typescript theme={null}
// From server/services/points-service.ts:28
const insertLedgerSQL = `
  INSERT INTO points_ledger (
    learner_id, 
    amount, 
    source_type, 
    source_id, 
    description
  )
  VALUES ($1, $2, $3, $4, $5)
  RETURNING id`;

await client.query(insertLedgerSQL, [
  opts.learnerId,
  opts.amount,
  opts.sourceType,
  opts.sourceId || null,
  opts.description,
]);
```

### Ledger Fields

| Field         | Type      | Description                                  |
| ------------- | --------- | -------------------------------------------- |
| `learner_id`  | integer   | Who earned/spent the points                  |
| `amount`      | integer   | Points earned (positive) or spent (negative) |
| `source_type` | enum      | Type of transaction                          |
| `source_id`   | string    | Reference to lesson, achievement, etc.       |
| `description` | text      | Human-readable description                   |
| `created_at`  | timestamp | When the transaction occurred                |

<Info>
  The ledger provides a complete audit trail of all point transactions. Parents can review the full history to understand how points were earned.
</Info>

## Awarding Points

The service ensures transactional integrity when awarding points:

```typescript theme={null}
// From server/services/points-service.ts:19
async awardPoints(opts: AwardPointsOptions): Promise<{ newBalance: number }> {
  if (opts.amount <= 0) throw new Error("Amount must be positive");

  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    // 1. Insert ledger entry
    await client.query(insertLedgerSQL, [...]);

    // 2. Update or create learner_points balance
    const upsertSQL = `
      INSERT INTO learner_points (learner_id, current_balance, total_earned, total_redeemed)
      VALUES ($1, $2, $2, 0)
      ON CONFLICT (learner_id)
      DO UPDATE SET 
        current_balance = learner_points.current_balance + EXCLUDED.current_balance,
        total_earned = learner_points.total_earned + EXCLUDED.current_balance,
        last_updated = NOW()
      RETURNING current_balance`;
      
    const { rows } = await client.query(upsertSQL, [opts.learnerId, opts.amount]);

    await client.query("COMMIT");
    return { newBalance: rows[0].current_balance };
  } catch (err) {
    await client.query("ROLLBACK");
    throw err;
  } finally {
    client.release();
  }
}
```

<Steps>
  <Step title="Validate Amount">
    Ensure points to award is a positive number
  </Step>

  <Step title="Start Transaction">
    Begin database transaction for atomicity
  </Step>

  <Step title="Record in Ledger">
    Insert permanent record of the transaction
  </Step>

  <Step title="Update Balance">
    Update current balance and total earned
  </Step>

  <Step title="Commit">
    Commit transaction if all steps succeed
  </Step>

  <Step title="Rollback on Error">
    Undo all changes if any step fails
  </Step>
</Steps>

## Balance Tracking

Each learner has three balance metrics:

```typescript theme={null}
// Database: learner_points table
interface LearnerPoints {
  learner_id: number;
  current_balance: number;    // Points available to spend
  total_earned: number;       // Lifetime points earned
  total_redeemed: number;     // Lifetime points spent
  last_updated: Date;
}
```

### Getting Current Balance

```typescript theme={null}
// From server/services/points-service.ts:62
async getBalance(learnerId: number | string): Promise<number> {
  const sql = `SELECT current_balance FROM learner_points WHERE learner_id = $1`;
  const { rows } = await pool.query(sql, [learnerId]);
  return rows.length ? Number(rows[0].current_balance) : 0;
}
```

<Note>
  If a learner has never earned points, `getBalance()` returns 0 rather than throwing an error.
</Note>

## Transaction History

Parents and learners can view recent point transactions:

```typescript theme={null}
// From server/services/points-service.ts:68
async getHistory(learnerId: number | string, limit = 50) {
  const sql = `
    SELECT * FROM points_ledger 
    WHERE learner_id = $1 
    ORDER BY created_at DESC 
    LIMIT $2`;
  const { rows } = await pool.query(sql, [learnerId, limit]);
  return rows;
}
```

Example response:

```json theme={null}
[
  {
    "id": "123",
    "learner_id": 42,
    "amount": 10,
    "source_type": "QUIZ_CORRECT",
    "source_id": "lesson_abc123",
    "description": "Correct answer on Math quiz",
    "created_at": "2026-03-12T10:30:00Z"
  },
  {
    "id": "122",
    "learner_id": 42,
    "amount": 50,
    "source_type": "LESSON_COMPLETE",
    "source_id": "lesson_abc123",
    "description": "Completed lesson: Introduction to Fractions",
    "created_at": "2026-03-12T10:28:00Z"
  }
]
```

## Integration with Rewards

Points can be saved toward specific rewards (see [Rewards System](/features/rewards)):

<Steps>
  <Step title="Earn Points">
    Learner completes lessons and quizzes to earn points
  </Step>

  <Step title="Delegate to Goal">
    Learner chooses a reward and delegates points from general balance to that goal
  </Step>

  <Step title="Request Redemption">
    When goal is reached, learner requests the reward
  </Step>

  <Step title="Parent Approval">
    Parent approves or rejects the redemption
  </Step>

  <Step title="Points Reset">
    On approval, saved points reset to 0 (reward is repeatable)
  </Step>
</Steps>

## Double-or-Loss Mode

An optional high-stakes mode for advanced learners:

<Warning>
  **Double-or-Loss Mode** is a special feature where:

  * Correct answers earn **2x points**
  * Wrong answers **deduct 1 point** from balance
  * Balance never goes below 0

  This mode should only be enabled for learners who are comfortable with the added pressure.
</Warning>

```typescript theme={null}
// From server/services/rewards-service.ts:539
export async function applyDoubleOrLossDeduction(
  learnerId: number,
  wrongCount: number
) {
  if (wrongCount <= 0) return;
  const deduction = wrongCount;

  const client = await pool.connect();
  try {
    await client.query('BEGIN');

    // Get current balance
    const { rows: balRows } = await client.query(
      `SELECT current_balance FROM learner_points WHERE learner_id=$1`,
      [learnerId]
    );
    const balance = balRows[0]?.current_balance ?? 0;
    
    // Never deduct more than current balance
    const actualDeduction = Math.min(deduction, balance);
    if (actualDeduction <= 0) {
      await client.query('COMMIT');
      return;
    }

    // Deduct points
    await client.query(
      `UPDATE learner_points
       SET current_balance = current_balance - $1, last_updated=NOW()
       WHERE learner_id=$2`,
      [actualDeduction, learnerId]
    );
    
    // Record in ledger
    await client.query(
      `INSERT INTO points_ledger (learner_id, amount, source_type, description)
       VALUES ($1, $2, 'DOUBLE_OR_LOSS_DEDUCTION', 'Wrong answer penalty')`,
      [learnerId, -actualDeduction]
    );

    await client.query('COMMIT');
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.release();
  }
}
```

## Typical Point Values

<Tabs>
  <Tab title="Quiz Answers">
    * **Correct answer**: 5-10 points (configurable)
    * **Double-or-loss correct**: 10-20 points (2x)
    * **Double-or-loss wrong**: -1 point
  </Tab>

  <Tab title="Lessons">
    * **Lesson completion**: 25-50 points (based on length)
    * **Perfect score bonus**: +10 points
    * **First lesson**: See [Achievements](/features/achievements)
  </Tab>

  <Tab title="Achievements">
    * **First Steps**: 100 points
    * **Learning Explorer**: 250 points
    * **Perfect Score**: 50 points
  </Tab>
</Tabs>

## API Examples

### Award Points

```typescript theme={null}
import { pointsService } from './services/points-service';

await pointsService.awardPoints({
  learnerId: 42,
  amount: 10,
  sourceType: 'QUIZ_CORRECT',
  sourceId: 'lesson_123_question_5',
  description: 'Correct answer on multiplication question'
});
```

### Check Balance

```typescript theme={null}
const balance = await pointsService.getBalance(42);
console.log(`Learner has ${balance} points`);
```

### View History

```typescript theme={null}
const history = await pointsService.getHistory(42, 20);
history.forEach(tx => {
  console.log(`${tx.created_at}: ${tx.amount} points - ${tx.description}`);
});
```

## Parent Controls

Parents can:

<CardGroup cols={2}>
  <Card title="View Full History" icon="list">
    See every point transaction with timestamps and descriptions
  </Card>

  <Card title="Manual Adjustments" icon="sliders">
    Award or deduct points with `ADMIN_ADJUST` source type
  </Card>

  <Card title="Enable Double-or-Loss" icon="toggle-on">
    Turn on high-stakes mode per learner
  </Card>

  <Card title="Create Rewards" icon="gift">
    Define point costs for real-world rewards
  </Card>
</CardGroup>

## Related Features

<CardGroup cols={3}>
  <Card title="Rewards System" icon="gift" href="/features/rewards">
    How learners redeem points for rewards
  </Card>

  <Card title="Achievements" icon="trophy" href="/features/achievements">
    Special milestone bonuses
  </Card>

  <Card title="Reports" icon="chart-bar" href="/features/reports">
    Visual analytics of point earning patterns
  </Card>
</CardGroup>
