From c3bf4ca5cfaa8db0a29e11edeebc19b439aded0c Mon Sep 17 00:00:00 2001 From: DungNT Date: Thu, 16 Apr 2026 23:13:57 +0700 Subject: [PATCH 1/3] Backend Code Challenger - solve problem 4 --- src/problem4/README.md | 19 +++++++++++++ src/problem4/package.json | 15 +++++++++++ src/problem4/src/index.ts | 43 ++++++++++++++++++++++++++++++ src/problem4/test/sum-to-n.test.ts | 20 ++++++++++++++ src/problem4/tsconfig.json | 17 ++++++++++++ 5 files changed, 114 insertions(+) create mode 100644 src/problem4/README.md create mode 100644 src/problem4/package.json create mode 100644 src/problem4/src/index.ts create mode 100644 src/problem4/test/sum-to-n.test.ts create mode 100644 src/problem4/tsconfig.json diff --git a/src/problem4/README.md b/src/problem4/README.md new file mode 100644 index 0000000000..b1ad7cc236 --- /dev/null +++ b/src/problem4/README.md @@ -0,0 +1,19 @@ +# Problem 4: Three Ways to Sum to n + +This folder provides three unique TypeScript implementations of `sum_to_n`. + +## Implementations + +- `sum_to_n_a`: iterative loop, simple and explicit, `O(|n|)` time and `O(1)` space. +- `sum_to_n_b`: arithmetic-series formula, fastest option at `O(1)` time and `O(1)` space. +- `sum_to_n_c`: recursive implementation, valid but less efficient because it uses `O(|n|)` stack space. + +For negative numbers, the functions sum from `0` down to `n` so the behavior stays deterministic for any integer input. + +## Test + +```bash +cd src/problem4 +npm install +npm test +``` diff --git a/src/problem4/package.json b/src/problem4/package.json new file mode 100644 index 0000000000..d74cb278cb --- /dev/null +++ b/src/problem4/package.json @@ -0,0 +1,15 @@ +{ + "name": "problem4", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "test": "node --import tsx --test ./test/sum-to-n.test.ts", + "build": "tsc -p tsconfig.json" + }, + "devDependencies": { + "@types/node": "^24.6.0", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + } +} diff --git a/src/problem4/src/index.ts b/src/problem4/src/index.ts new file mode 100644 index 0000000000..6d6d4ab60a --- /dev/null +++ b/src/problem4/src/index.ts @@ -0,0 +1,43 @@ +export function sum_to_n_a(n: number): number { + // Iterative approach: O(|n|) time, O(1) space. + let total = 0; + + if (n >= 0) { + for (let value = 1; value <= n; value += 1) { + total += value; + } + + return total; + } + + for (let value = 0; value >= n; value -= 1) { + total += value; + } + + return total; +} + +export function sum_to_n_b(n: number): number { + // Closed-form arithmetic series: O(1) time, O(1) space. + if (n >= 0) { + return (n * (n + 1)) / 2; + } + + return (Math.abs(n) * (n - 1)) / 2; +} + +export function sum_to_n_c(n: number): number { + // Recursive walk: O(|n|) time, O(|n|) call stack space. + const step = n >= 0 ? 1 : -1; + const stop = n >= 0 ? n + 1 : n - 1; + + const walk = (current: number, total: number): number => { + if (current === stop) { + return total; + } + + return walk(current + step, total + current); + }; + + return walk(step > 0 ? 1 : 0, 0); +} diff --git a/src/problem4/test/sum-to-n.test.ts b/src/problem4/test/sum-to-n.test.ts new file mode 100644 index 0000000000..b0eeec7955 --- /dev/null +++ b/src/problem4/test/sum-to-n.test.ts @@ -0,0 +1,20 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { sum_to_n_a, sum_to_n_b, sum_to_n_c } from "../src/index.js"; + +const cases = [ + { input: 0, expected: 0 }, + { input: 1, expected: 1 }, + { input: 5, expected: 15 }, + { input: -3, expected: -6 }, + { input: 10, expected: 55 } +] as const; + +for (const fn of [sum_to_n_a, sum_to_n_b, sum_to_n_c]) { + test(`${fn.name} returns the expected summation`, () => { + for (const { input, expected } of cases) { + assert.equal(fn(input), expected); + } + }); +} diff --git a/src/problem4/tsconfig.json b/src/problem4/tsconfig.json new file mode 100644 index 0000000000..17291bc86f --- /dev/null +++ b/src/problem4/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "declaration": true, + "outDir": "dist", + "rootDir": ".", + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} From c66df26646d89ab66b4b988fc6c15f5d7fd4349d Mon Sep 17 00:00:00 2001 From: DungNT Date: Thu, 16 Apr 2026 23:14:26 +0700 Subject: [PATCH 2/3] Backend Code Challenger - solve problem 5 --- src/problem5/.env.example | 3 + src/problem5/README.md | 223 ++++++++++++++ src/problem5/package.json | 29 ++ src/problem5/src/app.ts | 52 ++++ src/problem5/src/config/database.ts | 11 + src/problem5/src/config/env.ts | 11 + src/problem5/src/config/swagger.ts | 291 ++++++++++++++++++ .../src/controllers/resource-controller.ts | 157 ++++++++++ src/problem5/src/middleware/auth.ts | 35 +++ src/problem5/src/models/resource.ts | 45 +++ src/problem5/src/routes/resource-routes.ts | 19 ++ src/problem5/src/server.ts | 17 + src/problem5/src/types/express.d.ts | 9 + src/problem5/test/resource-api.test.ts | 98 ++++++ src/problem5/tsconfig.json | 17 + 15 files changed, 1017 insertions(+) create mode 100644 src/problem5/.env.example create mode 100644 src/problem5/README.md create mode 100644 src/problem5/package.json create mode 100644 src/problem5/src/app.ts create mode 100644 src/problem5/src/config/database.ts create mode 100644 src/problem5/src/config/env.ts create mode 100644 src/problem5/src/config/swagger.ts create mode 100644 src/problem5/src/controllers/resource-controller.ts create mode 100644 src/problem5/src/middleware/auth.ts create mode 100644 src/problem5/src/models/resource.ts create mode 100644 src/problem5/src/routes/resource-routes.ts create mode 100644 src/problem5/src/server.ts create mode 100644 src/problem5/src/types/express.d.ts create mode 100644 src/problem5/test/resource-api.test.ts create mode 100644 src/problem5/tsconfig.json diff --git a/src/problem5/.env.example b/src/problem5/.env.example new file mode 100644 index 0000000000..b64d96b9b5 --- /dev/null +++ b/src/problem5/.env.example @@ -0,0 +1,3 @@ +PORT=3000 +MONGODB_URI=mongodb://127.0.0.1:27017/code_challenge_problem5 +AUTH_TOKEN=replace-with-a-strong-token diff --git a/src/problem5/README.md b/src/problem5/README.md new file mode 100644 index 0000000000..a4f08e0518 --- /dev/null +++ b/src/problem5/README.md @@ -0,0 +1,223 @@ +# Problem 5: A Crude Server + +A RESTful CRUD API server built with Express.js and TypeScript, using MongoDB for persistence and Bearer token authentication for protected endpoints. + +## Features + +- CRUD operations for resource creation, listing, detail lookup, update, and deletion. +- Basic filtering with `name` and `status` query parameters. +- MongoDB persistence through Mongoose. +- Bearer token authentication controlled by environment configuration. +- Interactive Swagger/OpenAPI documentation for browser-based testing. +- TypeScript-based codebase with integration tests for the API flow. + +## Tech Stack + +- **Runtime**: Node.js +- **Framework**: Express.js +- **Language**: TypeScript +- **Database**: MongoDB with Mongoose +- **Documentation**: Swagger/OpenAPI 3.0 with Swagger UI +- **Testing**: Node test runner, Supertest, mongodb-memory-server + +## Resource Model + +```json +{ + "id": "ObjectId", + "name": "Test Resource", + "value": "xxxxx", + "description": "A test resource", + "status": 1, + "createdAt": "2026-01-25T10:00:00.000Z", + "updatedAt": "2026-01-25T10:00:00.000Z", + "detail": {} +} +``` + +## Endpoints + +- `POST /resources` creates a resource. +- `GET /resources?name=abc&status=1` lists resources with basic filters. +- `GET /resources/:id` returns a resource detail. +- `PUT /resources/:id` updates a resource. +- `DELETE /resources/:id` deletes a resource. + +All endpoints require `Authorization: Bearer `. + +## Installation + +1. Move into the problem folder: + +```bash +cd src/problem5 +``` + +2. Install dependencies: + +```bash +npm install +``` + +3. Copy the environment file: + +```bash +cp .env.example .env +``` + +4. Update the values in `.env`: + +```env +PORT=3000 +MONGODB_URI=mongodb://127.0.0.1:27017/code_challenge_problem5 +AUTH_TOKEN=replace-with-a-strong-token +``` + +## Running the Application + +### Development Mode + +```bash +npm run dev +``` + +### Production Build + +```bash +npm run build +``` + +If you want to start the compiled output manually: + +```bash +node dist/src/server.js +``` + +The server starts on `http://localhost:3000` by default. + +## API Documentation + +### Swagger UI + +Once the server is running, open: + +- [http://localhost:3000/api-docs](http://localhost:3000/api-docs) + +Swagger is public for convenience, but the actual CRUD endpoints still require Bearer authentication. + +How to test in Swagger UI: + +1. Open the Swagger page. +2. Expand any `/resources` endpoint. +3. Click `Authorize`. +4. Enter the token as plain text from `AUTH_TOKEN`. +5. Run requests directly from the browser. + +Useful supporting endpoint: + +- [http://localhost:3000/openapi.json](http://localhost:3000/openapi.json) exposes the raw OpenAPI document. + +## API Endpoints + +- `POST /resources` creates a resource. +- `GET /resources?name=abc&status=1` lists resources with basic filters. +- `GET /resources/:id` returns a resource detail. +- `PUT /resources/:id` updates a resource. +- `DELETE /resources/:id` deletes a resource. + +## Testing with cURL + +All examples below assume: + +```bash +set TOKEN=replace-with-a-strong-token +``` + +If you are using PowerShell, use: + +```powershell +$TOKEN = "replace-with-a-strong-token" +``` + +### 1. Create a resource + +```bash +curl -X POST http://localhost:3000/resources \ + -H "Authorization: Bearer %TOKEN%" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"Test Resource\",\"value\":\"xxxxx\",\"description\":\"A test resource\",\"status\":1,\"detail\":{\"source\":\"curl\"}}" +``` + +PowerShell example: + +```powershell +curl.exe -X POST http://localhost:3000/resources ` + -H "Authorization: Bearer $TOKEN" ` + -H "Content-Type: application/json" ` + -d "{\"name\":\"Test Resource\",\"value\":\"xxxxx\",\"description\":\"A test resource\",\"status\":1,\"detail\":{\"source\":\"powershell\"}}" +``` + +### 2. List resources + +```bash +curl "http://localhost:3000/resources?name=Test&status=1" \ + -H "Authorization: Bearer %TOKEN%" +``` + +### 3. Get resource detail + +```bash +curl http://localhost:3000/resources/ \ + -H "Authorization: Bearer %TOKEN%" +``` + +### 4. Update a resource + +```bash +curl -X PUT http://localhost:3000/resources/ \ + -H "Authorization: Bearer %TOKEN%" \ + -H "Content-Type: application/json" \ + -d "{\"description\":\"Updated description\",\"status\":0}" +``` + +### 5. Delete a resource + +```bash +curl -X DELETE http://localhost:3000/resources/ \ + -H "Authorization: Bearer %TOKEN%" +``` + +## Test + +```bash +npm test +``` + +## Project Structure + +```text +src/problem5/ +├── src/ +│ ├── config/ +│ │ ├── database.ts +│ │ ├── env.ts +│ │ └── swagger.ts +│ ├── controllers/ +│ │ └── resource-controller.ts +│ ├── middleware/ +│ │ └── auth.ts +│ ├── models/ +│ │ └── resource.ts +│ ├── routes/ +│ │ └── resource-routes.ts +│ ├── types/ +│ │ └── express.d.ts +│ ├── app.ts +│ └── server.ts +├── test/ +│ └── resource-api.test.ts +├── .env.example +├── package.json +├── tsconfig.json +└── README.md +``` diff --git a/src/problem5/package.json b/src/problem5/package.json new file mode 100644 index 0000000000..013a6212c7 --- /dev/null +++ b/src/problem5/package.json @@ -0,0 +1,29 @@ +{ + "name": "problem5", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/server.ts", + "test": "node --import tsx --test ./test/resource-api.test.ts", + "build": "tsc -p tsconfig.json" + }, + "dependencies": { + "dotenv": "^17.2.3", + "express": "^5.1.0", + "mongoose": "^8.19.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "@types/node": "^24.6.0", + "@types/supertest": "^6.0.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "mongodb-memory-server": "^10.2.3", + "supertest": "^7.1.4", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + } +} diff --git a/src/problem5/src/app.ts b/src/problem5/src/app.ts new file mode 100644 index 0000000000..d30eec0d68 --- /dev/null +++ b/src/problem5/src/app.ts @@ -0,0 +1,52 @@ +import express from "express"; +import swaggerUi from "swagger-ui-express"; + +import { swaggerSpec } from "./config/swagger.js"; +import { resourceRouter } from "./routes/resource-routes.js"; + +export function createApp() { + const app = express(); + app.use(express.json()); + app.get("/", (_request, response) => { + response.json({ + message: "Welcome to Problem 5 CRUD API", + documentation: "/api-docs", + openApiSpec: "/openapi.json" + }); + }); + app.get("/openapi.json", (_request, response) => { + response.json(swaggerSpec); + }); + app.use( + "/api-docs", + swaggerUi.serve, + swaggerUi.setup(swaggerSpec, { + customSiteTitle: "Problem 5 API Docs", + customCss: ".swagger-ui .topbar { display: none }" + }) + ); + app.use(resourceRouter); + + app.use((error: unknown, _request: express.Request, response: express.Response, _next: express.NextFunction) => { + if (error instanceof Error) { + response.status(400).json({ + success: false, + error: { + code: "BAD_REQUEST", + message: error.message + } + }); + return; + } + + response.status(500).json({ + success: false, + error: { + code: "INTERNAL_SERVER_ERROR", + message: "An unexpected error occurred." + } + }); + }); + + return app; +} diff --git a/src/problem5/src/config/database.ts b/src/problem5/src/config/database.ts new file mode 100644 index 0000000000..83b04475d7 --- /dev/null +++ b/src/problem5/src/config/database.ts @@ -0,0 +1,11 @@ +import mongoose from "mongoose"; + +import { env } from "./env.js"; + +export async function connectDatabase(uri = env.mongoUri) { + await mongoose.connect(uri); +} + +export async function disconnectDatabase() { + await mongoose.disconnect(); +} diff --git a/src/problem5/src/config/env.ts b/src/problem5/src/config/env.ts new file mode 100644 index 0000000000..324741e6b5 --- /dev/null +++ b/src/problem5/src/config/env.ts @@ -0,0 +1,11 @@ +import dotenv from "dotenv"; + +dotenv.config(); + +const defaultPort = 3000; + +export const env = { + authToken: process.env.AUTH_TOKEN ?? "change-me", + mongoUri: process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017/code_challenge_problem5", + port: Number(process.env.PORT ?? defaultPort) +}; diff --git a/src/problem5/src/config/swagger.ts b/src/problem5/src/config/swagger.ts new file mode 100644 index 0000000000..655da8a4c3 --- /dev/null +++ b/src/problem5/src/config/swagger.ts @@ -0,0 +1,291 @@ +import swaggerJsdoc from "swagger-jsdoc"; + +const swaggerDefinition: swaggerJsdoc.Options["definition"] = { + openapi: "3.0.3", + info: { + title: "Problem 5 CRUD API", + version: "1.0.0", + description: "A RESTful CRUD API built with Express.js, TypeScript, MongoDB, and Bearer token authentication." + }, + servers: [ + { + url: "http://localhost:3000", + description: "Local development server" + } + ], + tags: [ + { + name: "Resources", + description: "CRUD endpoints for resource management" + } + ], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "Token" + } + }, + schemas: { + Resource: { + type: "object", + properties: { + id: { + type: "string", + example: "680f4478d2ef40f533f51111" + }, + name: { + type: "string", + example: "Test Resource" + }, + value: { + type: "string", + example: "xxxxx" + }, + description: { + type: "string", + example: "A test resource" + }, + status: { + type: "integer", + enum: [0, 1], + example: 1 + }, + createdAt: { + type: "string", + format: "date-time" + }, + updatedAt: { + type: "string", + format: "date-time" + }, + detail: { + type: "object", + additionalProperties: true, + example: {} + } + }, + required: ["id", "name", "value", "description", "status", "createdAt", "updatedAt", "detail"] + }, + CreateResourceRequest: { + type: "object", + required: ["name", "value"], + properties: { + name: { + type: "string", + example: "Test Resource" + }, + value: { + type: "string", + example: "xxxxx" + }, + description: { + type: "string", + example: "A test resource" + }, + status: { + type: "integer", + enum: [0, 1], + example: 1 + }, + detail: { + type: "object", + additionalProperties: true, + example: { + scope: "integration-test" + } + } + } + }, + UpdateResourceRequest: { + type: "object", + properties: { + name: { + type: "string" + }, + value: { + type: "string" + }, + description: { + type: "string" + }, + status: { + type: "integer", + enum: [0, 1] + }, + detail: { + type: "object", + additionalProperties: true + } + } + }, + ErrorResponse: { + type: "object", + properties: { + success: { + type: "boolean", + example: false + }, + error: { + type: "object", + properties: { + code: { + type: "string", + example: "UNAUTHORIZED" + }, + message: { + type: "string", + example: "Missing Bearer token." + } + } + } + } + } + } + } +}; + +export const swaggerSpec = swaggerJsdoc({ + definition: swaggerDefinition, + apis: [] +}) as swaggerJsdoc.OAS3Definition & { + paths: Record; +}; + +swaggerSpec.paths = { + "/resources": { + get: { + tags: ["Resources"], + summary: "List resources", + security: [{ bearerAuth: [] }], + parameters: [ + { + in: "query", + name: "name", + schema: { type: "string" }, + description: "Case-insensitive partial match on resource name." + }, + { + in: "query", + name: "status", + schema: { type: "integer", enum: [0, 1] }, + description: "Filter by status." + } + ], + responses: { + "200": { + description: "Resources returned successfully." + }, + "401": { + description: "Unauthorized.", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + post: { + tags: ["Resources"], + summary: "Create a resource", + security: [{ bearerAuth: [] }], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/CreateResourceRequest" + } + } + } + }, + responses: { + "201": { + description: "Resource created successfully." + }, + "401": { + description: "Unauthorized." + } + } + } + }, + "/resources/{id}": { + get: { + tags: ["Resources"], + summary: "Get resource details", + security: [{ bearerAuth: [] }], + parameters: [ + { + in: "path", + name: "id", + required: true, + schema: { type: "string" } + } + ], + responses: { + "200": { + description: "Resource returned successfully." + }, + "404": { + description: "Resource not found." + } + } + }, + put: { + tags: ["Resources"], + summary: "Update a resource", + security: [{ bearerAuth: [] }], + parameters: [ + { + in: "path", + name: "id", + required: true, + schema: { type: "string" } + } + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/UpdateResourceRequest" + } + } + } + }, + responses: { + "200": { + description: "Resource updated successfully." + }, + "404": { + description: "Resource not found." + } + } + }, + delete: { + tags: ["Resources"], + summary: "Delete a resource", + security: [{ bearerAuth: [] }], + parameters: [ + { + in: "path", + name: "id", + required: true, + schema: { type: "string" } + } + ], + responses: { + "204": { + description: "Resource deleted successfully." + }, + "404": { + description: "Resource not found." + } + } + } + } +}; diff --git a/src/problem5/src/controllers/resource-controller.ts b/src/problem5/src/controllers/resource-controller.ts new file mode 100644 index 0000000000..0d46b9b951 --- /dev/null +++ b/src/problem5/src/controllers/resource-controller.ts @@ -0,0 +1,157 @@ +import type { Request, Response } from "express"; +import mongoose from "mongoose"; + +import { ResourceModel } from "../models/resource.js"; + +function buildPayload(document: unknown) { + return document; +} + +export async function createResource(request: Request, response: Response) { + const resource = await ResourceModel.create({ + name: request.body.name, + value: request.body.value, + description: request.body.description ?? "", + status: request.body.status ?? 1, + detail: request.body.detail ?? {} + }); + + response.status(201).json({ + success: true, + data: buildPayload(resource.toJSON()) + }); +} + +export async function listResources(request: Request, response: Response) { + const filters: Record = {}; + + if (typeof request.query.name === "string" && request.query.name.trim() !== "") { + filters.name = { + $regex: request.query.name.trim(), + $options: "i" + }; + } + + if (typeof request.query.status === "string" && request.query.status !== "") { + filters.status = Number(request.query.status); + } + + const items = await ResourceModel.find(filters).sort({ updatedAt: -1, createdAt: -1 }).lean(); + + response.json({ + success: true, + data: { + items: items.map((item) => ({ + ...item, + id: item._id.toString(), + _id: undefined + })), + total: items.length + } + }); +} + +export async function getResource(request: Request, response: Response) { + if (!mongoose.isValidObjectId(request.params.id)) { + response.status(404).json({ + success: false, + error: { + code: "RESOURCE_NOT_FOUND", + message: "Resource was not found." + } + }); + return; + } + + const resource = await ResourceModel.findById(request.params.id); + + if (!resource) { + response.status(404).json({ + success: false, + error: { + code: "RESOURCE_NOT_FOUND", + message: "Resource was not found." + } + }); + return; + } + + response.json({ + success: true, + data: buildPayload(resource.toJSON()) + }); +} + +export async function updateResource(request: Request, response: Response) { + if (!mongoose.isValidObjectId(request.params.id)) { + response.status(404).json({ + success: false, + error: { + code: "RESOURCE_NOT_FOUND", + message: "Resource was not found." + } + }); + return; + } + + const resource = await ResourceModel.findByIdAndUpdate( + request.params.id, + { + $set: { + ...(request.body.name !== undefined ? { name: request.body.name } : {}), + ...(request.body.value !== undefined ? { value: request.body.value } : {}), + ...(request.body.description !== undefined ? { description: request.body.description } : {}), + ...(request.body.status !== undefined ? { status: request.body.status } : {}), + ...(request.body.detail !== undefined ? { detail: request.body.detail } : {}) + } + }, + { + new: true, + runValidators: true + } + ); + + if (!resource) { + response.status(404).json({ + success: false, + error: { + code: "RESOURCE_NOT_FOUND", + message: "Resource was not found." + } + }); + return; + } + + response.json({ + success: true, + data: buildPayload(resource.toJSON()) + }); +} + +export async function deleteResource(request: Request, response: Response) { + if (!mongoose.isValidObjectId(request.params.id)) { + response.status(404).json({ + success: false, + error: { + code: "RESOURCE_NOT_FOUND", + message: "Resource was not found." + } + }); + return; + } + + const resource = await ResourceModel.findByIdAndDelete(request.params.id); + + if (!resource) { + response.status(404).json({ + success: false, + error: { + code: "RESOURCE_NOT_FOUND", + message: "Resource was not found." + } + }); + return; + } + + response.status(204).send(); +} diff --git a/src/problem5/src/middleware/auth.ts b/src/problem5/src/middleware/auth.ts new file mode 100644 index 0000000000..f27297888b --- /dev/null +++ b/src/problem5/src/middleware/auth.ts @@ -0,0 +1,35 @@ +import type { NextFunction, Request, Response } from "express"; + +import { env } from "../config/env.js"; + +export function requireAuth(request: Request, response: Response, next: NextFunction) { + const authorization = request.header("Authorization"); + const expectedToken = process.env.AUTH_TOKEN ?? env.authToken; + + if (!authorization?.startsWith("Bearer ")) { + response.status(401).json({ + success: false, + error: { + code: "UNAUTHORIZED", + message: "Missing Bearer token." + } + }); + return; + } + + const token = authorization.slice("Bearer ".length); + + if (token !== expectedToken) { + response.status(401).json({ + success: false, + error: { + code: "INVALID_TOKEN", + message: "Auth token is invalid." + } + }); + return; + } + + request.authToken = token; + next(); +} diff --git a/src/problem5/src/models/resource.ts b/src/problem5/src/models/resource.ts new file mode 100644 index 0000000000..a0039f4bc2 --- /dev/null +++ b/src/problem5/src/models/resource.ts @@ -0,0 +1,45 @@ +import { Schema, model, type InferSchemaType } from "mongoose"; + +const resourceSchema = new Schema( + { + name: { + type: String, + required: true, + trim: true + }, + value: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + default: "", + trim: true + }, + status: { + type: Number, + enum: [0, 1], + default: 1 + }, + detail: { + type: Schema.Types.Mixed, + default: {} + } + }, + { + timestamps: true, + versionKey: false, + toJSON: { + transform: (_doc, ret: Record) => { + ret.id = String(ret._id); + ret._id = undefined; + return ret; + } + } + } +); + +export type ResourceDocument = InferSchemaType; + +export const ResourceModel = model("Resource", resourceSchema); diff --git a/src/problem5/src/routes/resource-routes.ts b/src/problem5/src/routes/resource-routes.ts new file mode 100644 index 0000000000..9ac6d09316 --- /dev/null +++ b/src/problem5/src/routes/resource-routes.ts @@ -0,0 +1,19 @@ +import { Router } from "express"; + +import { + createResource, + deleteResource, + getResource, + listResources, + updateResource +} from "../controllers/resource-controller.js"; +import { requireAuth } from "../middleware/auth.js"; + +export const resourceRouter = Router(); + +resourceRouter.use(requireAuth); +resourceRouter.post("/resources", createResource); +resourceRouter.get("/resources", listResources); +resourceRouter.get("/resources/:id", getResource); +resourceRouter.put("/resources/:id", updateResource); +resourceRouter.delete("/resources/:id", deleteResource); diff --git a/src/problem5/src/server.ts b/src/problem5/src/server.ts new file mode 100644 index 0000000000..c21c28aaeb --- /dev/null +++ b/src/problem5/src/server.ts @@ -0,0 +1,17 @@ +import { connectDatabase } from "./config/database.js"; +import { env } from "./config/env.js"; +import { createApp } from "./app.js"; + +async function bootstrap() { + await connectDatabase(); + + const app = createApp(); + app.listen(env.port, () => { + console.log(`Problem 5 API listening on port ${env.port}`); + }); +} + +bootstrap().catch((error) => { + console.error("Failed to start server", error); + process.exitCode = 1; +}); diff --git a/src/problem5/src/types/express.d.ts b/src/problem5/src/types/express.d.ts new file mode 100644 index 0000000000..ec787da7e0 --- /dev/null +++ b/src/problem5/src/types/express.d.ts @@ -0,0 +1,9 @@ +declare global { + namespace Express { + interface Request { + authToken?: string; + } + } +} + +export {}; diff --git a/src/problem5/test/resource-api.test.ts b/src/problem5/test/resource-api.test.ts new file mode 100644 index 0000000000..f30d368ec2 --- /dev/null +++ b/src/problem5/test/resource-api.test.ts @@ -0,0 +1,98 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import mongoose from "mongoose"; +import { MongoMemoryServer } from "mongodb-memory-server"; +import request from "supertest"; + +import { createApp } from "../src/app.js"; + +const authToken = "test-token"; + +test("resource API enforces auth and supports CRUD flows", async () => { + const mongoServer = await MongoMemoryServer.create(); + + try { + process.env.MONGODB_URI = mongoServer.getUri(); + process.env.AUTH_TOKEN = authToken; + + await mongoose.connect(process.env.MONGODB_URI); + + const app = createApp(); + const agent = request(app); + + await agent.get("/resources").expect(401); + + const createResponse = await agent + .post("/resources") + .set("Authorization", `Bearer ${authToken}`) + .send({ + name: "Test Resource", + value: "xxxxx", + description: "A test resource", + status: 1, + detail: { + scope: "integration-test" + } + }) + .expect(201); + + assert.equal(createResponse.body.success, true); + assert.equal(createResponse.body.data.name, "Test Resource"); + assert.ok(createResponse.body.data.id); + + const resourceId = createResponse.body.data.id as string; + + const listResponse = await agent + .get("/resources?status=1&name=Test") + .set("Authorization", `Bearer ${authToken}`) + .expect(200); + + assert.equal(listResponse.body.data.items.length, 1); + assert.equal(listResponse.body.data.total, 1); + + const detailResponse = await agent + .get(`/resources/${resourceId}`) + .set("Authorization", `Bearer ${authToken}`) + .expect(200); + + assert.equal(detailResponse.body.data.id, resourceId); + + const updateResponse = await agent + .put(`/resources/${resourceId}`) + .set("Authorization", `Bearer ${authToken}`) + .send({ + description: "Updated description", + status: 0 + }) + .expect(200); + + assert.equal(updateResponse.body.data.description, "Updated description"); + assert.equal(updateResponse.body.data.status, 0); + + await agent + .delete(`/resources/${resourceId}`) + .set("Authorization", `Bearer ${authToken}`) + .expect(204); + + await agent + .get(`/resources/${resourceId}`) + .set("Authorization", `Bearer ${authToken}`) + .expect(404); + } finally { + await mongoose.disconnect(); + await mongoServer.stop(); + } +}); + +test("swagger documentation is publicly accessible", async () => { + const app = createApp(); + const agent = request(app); + + const docsResponse = await agent.get("/api-docs/").expect(200); + assert.match(docsResponse.text, /swagger-ui/i); + + const specResponse = await agent.get("/openapi.json").expect(200); + assert.equal(specResponse.body.openapi, "3.0.3"); + assert.ok(specResponse.body.paths["/resources"]); +}); diff --git a/src/problem5/tsconfig.json b/src/problem5/tsconfig.json new file mode 100644 index 0000000000..17291bc86f --- /dev/null +++ b/src/problem5/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "declaration": true, + "outDir": "dist", + "rootDir": ".", + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} From 77794fd82fdeaa5ae6bcb34940c2d24deab40240 Mon Sep 17 00:00:00 2001 From: DungNT Date: Thu, 16 Apr 2026 17:37:33 +0100 Subject: [PATCH 3/3] Backend Code Challenger - solve problem 6 --- .gitignore | 5 + src/problem6/README.md | 622 ++++++++++++++++++ src/problem6/diagram/execution-flow.png | Bin 0 -> 81210 bytes src/problem6/diagram/execution-flow.puml | 53 ++ .../diagram/high-level-architecture.png | Bin 0 -> 36980 bytes .../diagram/high-level-architecture.puml | 34 + 6 files changed, 714 insertions(+) create mode 100644 .gitignore create mode 100644 src/problem6/README.md create mode 100644 src/problem6/diagram/execution-flow.png create mode 100644 src/problem6/diagram/execution-flow.puml create mode 100644 src/problem6/diagram/high-level-architecture.png create mode 100644 src/problem6/diagram/high-level-architecture.puml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..ef5a800e1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +coverage/ +package-lock.json diff --git a/src/problem6/README.md b/src/problem6/README.md new file mode 100644 index 0000000000..8d16cc2524 --- /dev/null +++ b/src/problem6/README.md @@ -0,0 +1,622 @@ +# Problem 6: Architecture Specification + +This document specifies a backend API module for a live leaderboard system. It is written for a backend engineering team that will implement the service. + +## Table of Contents + +- [Problem 6: Architecture Specification](#problem-6-architecture-specification) + - [Table of Contents](#table-of-contents) + - [1. Scope](#1-scope) + - [2. High-Level Architecture](#2-high-level-architecture) + - [3. High-Level Architecture Diagram](#3-high-level-architecture-diagram) + - [4. Functional Requirements](#4-functional-requirements) + - [5. Data Model](#5-data-model) + - [6. API Specification](#6-api-specification) + - [6.1 Login](#61-login) + - [6.2 Query Leaderboard](#62-query-leaderboard) + - [6.3 List Available Tasks](#63-list-available-tasks) + - [6.4 List User Tasks By Status](#64-list-user-tasks-by-status) + - [6.5 Request Action](#65-request-action) + - [6.6 Submit Action Completion](#66-submit-action-completion) + - [6.7 Get User Profile \& Score](#67-get-user-profile--score) + - [7. WebSocket Specification](#7-websocket-specification) + - [7.1 `leaderboard:event`](#71-leaderboardevent) + - [7.2 `user:event`](#72-userevent) + - [7.3 `user_task:event`](#73-user_taskevent) + - [8. Execution Flow Diagrams](#8-execution-flow-diagrams) + - [9. Security \& Authorization](#9-security--authorization) + - [JWT](#jwt) + - [Password Storage](#password-storage) + - [Score Update Protection](#score-update-protection) + - [Rate Limiting](#rate-limiting) + - [Additional Controls](#additional-controls) + - [10. Non-Functional Expectations](#10-non-functional-expectations) + - [11. Suggested Implementation Notes](#11-suggested-implementation-notes) + - [12. Future Improvements](#12-future-improvements) + +## 1. Scope + +The module supports: + +- user authentication with JWT; +- task discovery and task execution requests; +- score updates after a task is completed; +- prevention of duplicate task requests for the same user and task; +- prevention of duplicate task completion and duplicate score awards; +- live leaderboard updates through WebSocket; +- protection against unauthorized score changes. + +Out of scope: + +- the internal business logic of how a user completes a task; +- frontend implementation details; +- admin tooling. + +## 2. High-Level Architecture + +The application server is intentionally split into two logical services inside the backend API: + +- `Auth Service`: validates credentials, issues JWT, validates JWT for HTTP and WebSocket connections. +- `Score Service`: serves leaderboard/task endpoints, validates task actions, updates scores, refreshes cache, and emits real-time events. + +Supporting infrastructure: + +- `MySQL`: source of truth for users, tasks, and user task state. +- `Redis`: distributed rate limiting and cached top-10 leaderboard. +- `WebSocket Gateway`: authenticated real-time channel for leaderboard, user, and user-task events. + +## 3. High-Level Architecture Diagram + +PlantUML source: [high-level-architecture.puml](./diagram/high-level-architecture.puml) + +![High-Level Architecture](./diagram/high-level-architecture.png) + +## 4. Functional Requirements + +1. The website shows the top users by score, defaulting to top 10. +2. The leaderboard must update live without requiring manual refresh. +3. A user can request an available task/action. +4. Completing that action triggers an API call that may increase the user score. +5. Unauthorized or malicious score updates must be rejected. + +## 5. Data Model + +Recommended relational schema: + +```sql +CREATE TABLE users ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + email VARCHAR(255) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + nick_name VARCHAR(50) UNIQUE NOT NULL, + score BIGINT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE tasks ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + name VARCHAR(255) NOT NULL, + description TEXT, + status SMALLINT NOT NULL DEFAULT 1, -- 0 inactive, 1 active + score BIGINT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE user_tasks ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + user_id CHAR(36) NOT NULL, + task_id CHAR(36) NOT NULL, + status SMALLINT NOT NULL DEFAULT 1, -- 1 inprogress, 2 complete, 9 expired + score BIGINT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_user_tasks_user FOREIGN KEY (user_id) REFERENCES users(id), + CONSTRAINT fk_user_tasks_task FOREIGN KEY (task_id) REFERENCES tasks(id) +); + +CREATE UNIQUE INDEX ux_user_tasks_user_task + ON user_tasks(user_id, task_id); + +CREATE INDEX ix_users_score_desc + ON users(score DESC, updated_at DESC); + +CREATE INDEX ix_tasks_status_updated_at + ON tasks(status, updated_at DESC); + +CREATE INDEX ix_user_tasks_user_status_updated_at + ON user_tasks(user_id, status, updated_at DESC); +``` + +Notes: + +- `users.score` is the current aggregated score used for ranking. +- `tasks.score` is the amount added to the user score when the task is successfully completed. +- `user_tasks.score` stores the awarded score snapshot for audit/debugging after completion. +- `user_tasks(user_id, task_id)` must remain unique to stop duplicate claims for the same task. + +## 6. API Specification + +All protected endpoints require: + +```http +Authorization: Bearer +``` + +### 6.1 Login + +`POST /auth/login` + +Request: + +```json +{ + "email": "player1@example.com", + "password": "user-input-password" +} +``` + +Response: + +```json +{ + "success": true, + "data": { + "token": "jwt_token", + "expiresIn": 3600, + "user": { + "userId": "uuid-123", + "nick_name": "player1", + "email": "player1@example.com", + "score": 15000 + } + } +} +``` + +Behavior: + +- receive the user's password over TLS and compare it against the stored password hash; +- issue signed JWT containing `sub`, `email`, `nick_name`; +- reject invalid credentials with `401 INVALID_CREDENTIALS`. + +**Note:** **Passwords must never be stored or transmitted as reusable plaintext outside the immediate login request. The persistence layer should store only a password hash, and the recommended hashing algorithms are `Argon2id` or `bcrypt`, not MD5 or reversible encryption.** + +### 6.2 Query Leaderboard + +`GET /leaderboard?top=xx` + +Rules: + +- default `top = 10`; +- maximum `top = 100`; +- sort by `score DESC`, then `updated_at ASC` or `id ASC` for deterministic ranking; +- prefer Redis cache for top 10, fallback to MySQL for non-default `top`. + +Response: + +```json +{ + "success": true, + "data": { + "leaderboard": [ + { + "rank": 1, + "userId": "uuid-123", + "nick_name": "player1", + "email": "player1@example.com", + "score": 15000 + }, + { + "rank": 2, + "userId": "uuid-456", + "nick_name": "player2", + "email": "player2@example.com", + "score": 12500 + } + ], + "lastUpdated": "2026-01-25T10:30:00Z" + } +} +``` + +Errors: + +- `400 INVALID_TOP_LIMIT` when `top < 1` or `top > 100`; +- `401 UNAUTHORIZED` when JWT is missing or invalid. + +### 6.3 List Available Tasks + +`GET /tasks?page=1&limit=20` + +Behavior: + +- return active tasks not yet present in `user_tasks` for the authenticated user; +- sort by `tasks.updated_at DESC`; +- support pagination with `page` and `limit`; +- default `page = 1`, default `limit = 20`, maximum `limit = 100`. + +Response: + +```json +{ + "success": true, + "data": { + "page": 1, + "limit": 20, + "total": 1, + "items": [ + { + "taskId": "uuid-task-1", + "name": "Test action", + "description": "A test description", + "status": 1, + "score": 100, + "updatedAt": "2026-01-25T10:30:00Z" + } + ] + } +} +``` + +### 6.4 List User Tasks By Status + +`GET /user-tasks?status=xx&page=1&limit=20` + +Behavior: + +- query `user_tasks` for the authenticated user; +- optional `status` filter; +- sort by `updated_at DESC`; +- support pagination with `page` and `limit`; +- default `page = 1`, default `limit = 20`, maximum `limit = 100`. + +Response: + +```json +{ + "success": true, + "data": { + "page": 1, + "limit": 20, + "total": 1, + "items": [ + { + "userTaskId": "uuid-user-task-1", + "taskId": "uuid-task-1", + "status": 1, + "score": 0, + "updatedAt": "2026-01-25T10:30:00Z" + } + ] + } +} +``` + +### 6.5 Request Action + +`POST /task/request-action/:task_id` + +Request: + +```json +{ + "action_type": "start_action" +} +``` + +Validation: + +- verify `task_id` exists in `tasks`; +- verify task is active; +- verify `(user_id, task_id)` does not already exist in `user_tasks`; +- enforce one active/requested record per `(user_id, task_id)` so the same task cannot be started twice by the same user; +- reject bad `action_type`. + +Success behavior: + +- insert a new `user_tasks` row with `status = 1` (`inprogress`); +- return inserted `user_tasks`; +- emit `user_task:event`. + +Duplicate-prevention requirements: + +- the service must reject duplicate start requests for the same `(user_id, task_id)`; +- the unique index on `user_tasks(user_id, task_id)` is the final database guardrail; +- the API should still handle race conditions explicitly and translate unique-key conflicts into a business error response instead of a generic `500`. + +Suggested errors: + +- `404 TASK_NOT_FOUND` +- `409 TASK_ALREADY_REQUESTED` +- `409 TASK_INACTIVE` +- `409 TASK_DUPLICATE_REQUEST` +- `400 INVALID_ACTION_TYPE` + +### 6.6 Submit Action Completion + +`POST /task/submit-action/:user_task_id` + +Request: + +```json +{ + "action_type": "complete_action" +} +``` + +Validation: + +- verify `user_task_id` exists; +- verify the row belongs to the authenticated user; +- verify current status is `1` (`inprogress`); +- reject any completion attempt for a task that is already `complete` or otherwise no longer eligible for scoring; +- reject invalid `action_type`. + +Success behavior: + +1. start a database transaction; +2. set `user_tasks.status = 2`; +3. copy awarded score into `user_tasks.score`; +4. increment `users.score` atomically by `tasks.score`; +5. commit transaction; +6. refresh top-10 leaderboard cache in Redis; +7. if the top 10 changed, broadcast `leaderboard:event`; +8. emit `user:event` and `user_task:event`; +9. return updated `user_tasks` and user profile. + +Duplicate-prevention requirements: + +- task completion must be idempotent from the scoring perspective: the same `user_task` must never award points more than once; +- the transaction must lock/read the current `user_tasks` row and abort if the status is no longer `inprogress`; +- if two completion requests arrive concurrently, only one may commit the score update, and the other must receive a deterministic conflict error; +- leaderboard cache refresh and WebSocket emission must happen only after the single successful completion commit. + +Multi-instance handling in detail: + +1. each API service instance may receive the same completion request at nearly the same time, so duplicate protection cannot rely on in-memory state; +2. the source of truth must be the shared MySQL database, using a single transaction on the same `user_tasks` row; +3. inside the transaction, the service should run `SELECT ... FOR UPDATE` on the target `user_tasks` record so only one API service instance can hold the row lock at a time; +4. the first API service instance that acquires the lock re-checks `status = 1` (`inprogress`), updates `user_tasks.status = 2`, writes `user_tasks.score`, and increments `users.score`; +5. any other API service instance waits for the lock, then reads the already-updated row, sees that `status != 1`, and must return a conflict such as `409 USER_TASK_ALREADY_COMPLETED` or `409 USER_TASK_DUPLICATE_COMPLETION`; +6. because the score increment happens in the same transaction as the status change, only one API service instance can ever commit the score award; +7. Redis cache refresh and WebSocket broadcasting must happen only after the winning transaction commits successfully, so downstream systems also observe a single completion event. + +Recommended transaction outline: + +```sql +BEGIN; + +SELECT id, user_id, task_id, status, score +FROM user_tasks +WHERE id = ? +FOR UPDATE; + +-- Application checks: +-- 1. row exists +-- 2. row belongs to authenticated user +-- 3. status = 1 (inprogress) + +SELECT id, score +FROM tasks +WHERE id = ? + AND status = 1; + +UPDATE user_tasks +SET status = 2, + score = ? +WHERE id = ? + AND status = 1; + +UPDATE users +SET score = score + ? +WHERE id = ?; + +COMMIT; +``` + +Recommended API-side safeguards in addition to row locking: + +- attach an idempotency key or request ID to the completion request for safer retry behavior across gateways, timeouts, or client retries; +- log the winning completion request ID together with `user_task_id`; +- if `UPDATE user_tasks ... WHERE status = 1` affects `0` rows, treat it as a duplicate/already-completed request instead of retrying blindly; +- never publish leaderboard or user events before the database commit succeeds. + +Success response: + +```json +{ + "success": true, + "data": { + "userTask": { + "userTaskId": "uuid-user-task-1", + "taskId": "uuid-task-1", + "status": 2, + "score": 100 + }, + "user": { + "userId": "uuid-123", + "nick_name": "player1", + "email": "player1@example.com", + "score": 15100 + } + } +} +``` + +Suggested errors: + +- `404 USER_TASK_NOT_FOUND` +- `403 USER_TASK_FORBIDDEN` +- `409 USER_TASK_INVALID_STATUS` +- `409 USER_TASK_ALREADY_COMPLETED` +- `409 USER_TASK_DUPLICATE_COMPLETION` +- `400 INVALID_ACTION_TYPE` + +### 6.7 Get User Profile & Score + +`GET /users/me` + +Response: + +```json +{ + "success": true, + "data": { + "userId": "uuid-123", + "nick_name": "player1", + "email": "player1@example.com", + "score": 15100, + "createdAt": "2026-01-25T10:00:00Z", + "updatedAt": "2026-01-25T10:35:00Z" + } +} +``` + +## 7. WebSocket Specification + +Connection: + +```ts +const socket = io("wss://xxx.xxx", { + auth: { + token: "jwt_token" + } +}); +``` + +Authentication: + +- the WebSocket gateway verifies the JWT during handshake; +- if verification fails, the connection is rejected; +- the authenticated `userId` becomes the room/channel key for user-scoped events. + +Events: + +### 7.1 `leaderboard:event` + +Broadcast only when cached top 10 changes. + +```json +{ + "leaderboard": [ + { + "rank": 1, + "userId": "uuid-123", + "nick_name": "player1", + "email": "player1@example.com", + "score": 15100 + } + ], + "updatedAt": "2026-01-25T10:33:00Z" +} +``` + +### 7.2 `user:event` + +Emit only to the affected user. + +```json +{ + "userId": "uuid-123", + "nick_name": "player1", + "email": "player1@example.com", + "score": 5700, + "rank": 15 +} +``` + +### 7.3 `user_task:event` + +Emit only to the affected user. + +```json +{ + "userId": "uuid-123", + "taskId": "uuid-task-1", + "status": 2, + "task": { + "name": "Test action", + "description": "A test description", + "status": 1, + "score": 100 + } +} +``` + +## 8. Execution Flow Diagrams + +PlantUML source: [execution-flow.puml](./diagram/execution-flow.puml) + +![Execution Flow](./diagram/execution-flow.png) + +End-to-end flow: + +1. User logs in and receives a JWT. +2. Client opens WebSocket connection with that JWT. +3. Client loads available tasks through `GET /tasks`. +4. User chooses a task and requests it through `POST /task/request-action/:task_id`. +5. Service creates an `inprogress` record in `user_tasks`. +6. Client completes the task and submits `POST /task/submit-action/:user_task_id`. +7. Score service validates ownership and current status. +8. Score service updates `user_tasks` and `users.score` inside one transaction. +9. Service refreshes top-10 leaderboard cache. +10. If the top 10 changes, the service broadcasts `leaderboard:event`. +11. Service also emits `user:event` and `user_task:event` to the affected user. + +## 9. Security & Authorization + +### JWT + +- login issues a signed JWT using a strong server-side secret or asymmetric key pair; +- JWT payload should include `sub` as the user ID plus minimal identity claims; +- JWT expiration should be short-lived, for example 15 to 60 minutes; +- every protected HTTP request and WebSocket connection must verify signature, expiration, and subject. + +### Password Storage + +- store password hashes only, never plain text; +- use Argon2id or bcrypt with current secure cost parameters. + +### Score Update Protection + +- never allow direct score update endpoints from the client; +- only `submit-action` can increase score; +- `submit-action` must validate task ownership, task status, and current user identity; +- the transaction must update score exactly once for each `user_task`. + +### Rate Limiting + +- use Redis-backed distributed global rate limiting so limits hold across multiple API instances; +- apply stricter limits to login and task submission endpoints; +- recommended dimensions: `ip`, `userId`, and route key; +- return `429 TOO_MANY_REQUESTS` when the limit is exceeded. + +### Additional Controls + +- audit every rejected completion attempt with user ID, task ID, and reason; +- validate UUID input and enum/status input strictly; +- use idempotency protection for completion requests where feasible; +- use TLS for HTTP and WebSocket traffic. + +## 10. Non-Functional Expectations + +- leaderboard read latency should remain low for top-10 queries because Redis serves the hot path; +- score updates must be atomic and consistent under concurrent submissions; +- event payloads should be small and stable because they fan out to many clients; +- API responses should always use deterministic error codes for client handling. + +## 11. Suggested Implementation Notes + +- prefer MySQL transactions with `SELECT ... FOR UPDATE` or equivalent row locking around `user_tasks` and `users`; +- keep the leaderboard cache write-through or refresh-on-change, not periodic-only; +- centralize auth logic so HTTP and WebSocket use the same JWT verification function; +- keep WebSocket broadcasting in a dedicated gateway/adapter instead of embedding socket logic in controllers. + +## 12. Future Improvements + +- add refresh tokens and token revocation for stronger session control; +- add task expiration rules and scheduled cleanup for stale `inprogress` rows; +- add audit/event tables if fraud investigation becomes important; +- add observability around transaction retries, cache hit rate, and WebSocket delivery failures. diff --git a/src/problem6/diagram/execution-flow.png b/src/problem6/diagram/execution-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..520a0ae215905edbccbd6fe2740aa497ca925288 GIT binary patch literal 81210 zcmcfpc|6qZ_Xmz^FBRQXLg=m#LfNyni!7DwYuVS4eXMPggf@u@MaaJIYgxnCjonx> z_H~#s#+dIJ)qUUZ&*S&|{`vJ!%`mUm^}4Qep67X<=bQ<=rKWh0o{gT4j_#n+jjMO) z=yoj7(e2v1cRReJ8R+H&|M0t9({V9HI3n%LEnMgn%^l30?zxzoow$!YVddiD=qxQP z>}Yq-!Nt|yPRJBt@7CPF1(#v6zN_Q%=Y6_ua2=12d13nM{pSwXVJUne9yb1;;tz<8 z-+OFS`%Lfl4Qu09uVy}AzUPS=e=kmb)h+su-bo^ACaKz&UppNkTzRukWucwb<&3<$ z+2QK@iyl4dCTj7|pL;LX9B66Px@G+%laV(s^w^IR)$RFz;oZ*fXgF4I^rP?i^tqIh zj{=UJ<&O_z#M^_snF?9&d5#RoCk-31F9q#Bw{PWw_~Zf8S4@bg){pE<*}=T7%3&3m z4A;(`6-b$GZ&-?%A5=O+8FJ34889rAJ}-Y}&Lrs5K}Et*wEJQH?wnh@?q|xV2a9Cv z=Ra-O(9r#NfY?Bfhe!RV+1pYxceB*3N{>gKwv!K<-9gt`7K*;GJ0`C4+LMXwXOw9y zFSzhabt^pKvWG~#vg}>r zC4J;QJ30|B*h`LHxKsV@XXzk|kxbcN2R?pocwZ#P7t_69=szVTq-KAdojloi;eA&W zy=ZF4p~P#C{e*jh#52!bzm@DPHS^dzCh*H~(SrpVRr^1e`G4Pzuicp*&!Kj6%0z43 z#5P%LO@waNJrj|c@=~w+Xg&OpiM{B1 zF|z}^@^=ST^;&22mq^X!9kuekdBjEGN&)Jl@#%^1MW1s|90z#PP*cZJ3C|u}G-!Sv z&&Xl>R$uRy$D+r1iF%<_y&NU>_BIFR>&b_%r>NebxXJTfTU0F(>lS(;tPr)5)w~!{ z`Ei0-8?lLY#HNS&55XmIlTj}g$KIB_qnRQV(L7Gx}|&&@%*zSAim|vcFv!8U9T(xvU#Xwh)kjohGNQEawMG{c7SivRW_M1sM7vJ z_Yl4H2Au@s*2^B*t$+Ual?nd8zx(~_e?R!Yuju~YF7x|OX_qeMMt1{G)AWJ*#BHT{A!?j^>avc9_65__cetX@QtgF4rY+jv7* zb#?UxF8tb9XhCkS%1ngCaV+=S_T{Ros$IKwZEDN7&ktOv2n_Rh`{pSfZB??FH^NS< zaN5fV3JRV&WjZ|_V=qzRGIhr%EIPUzfh`xe#aPM8%6dCGE`G~F8h%yzs%MIy!KCR^ z&!Z&lzvH&iq4v-f^7g+k78et<`jVA(dkiDioo?7s7ZMWEKSo3e@bjz1$j_mbefmr5 zdQp*)cXf1ikyGv5#KR#f-S#JUH%Bl-mn zU7ej)%Hibkd4aJz&g$x+3$CIE4jxRBm6spl*UhsDvoq>Ig{mdyBqulcqzq~|jnS_$ z@5f3YrjK)SO3cNLuXilZ&vjrhN12#ne~2xu&U90B&zs`iHy+iSzGSOCKW0NOc$Z{t z{~8nFbDS;{N%tXM0ugxE!6Ci2v7teQl%R@QxlQjnT)8d9_UhHEPYoO|uX=ryJ9VmR zXzX%Fq55%lc3XGl(A5v9rR)hL9%udK%a^Nu;+#j0m?wNx@GC!z?Q|r}>IUX%2Fmsq zrq+c$NFuE*jmcxsm+8}8JG4`@Gv1RM2IsI1E^}q7%gf8&sUOJn9=1>S^_E_LFKo=o z%xp^UI^HojI9D<*m#?m;7pZxYM6j&c$U6!B>F3+Et@-a~5sMBBtC44* z9c_~vD*O?vpNNnE@eQ-Pn;TnVsoPv%O%ThKD_7UzCAJ@4GP)85e$5D*|E zE&Wn{h?6RQ;>?*FB8lQfzLfPBM+D9$uH>^_N7m%q_bcDLY2JbxYl~CVB!LNtm&5h? z$K2885y{l*K;~sH)2ES@{SAeksTrkdsQr0)d3thjwl9qKxS?( zW#axkU%!5BEo=}^7Z3}stEsW}218@hM&*R=pt)<#AkM#2Lx`JKNI*b?6fNh)z#;wR zH5w%u!)i%d9$!Cmel^FJE2nWIIVHu|V`P`$ZHB}*$;tepSrOV;J+-Mc&bZR$?^#*r zZ+u!QO-KBZQX@DtszFE9MEozht>@^&sehFE9Qr2b-Mhm--o6y&QAW%!=sMS6ONu8In*CrZxs>cnZ1697 zA}=dD`@r15hLGjp)_MQVos^hBmtGg0_H`6nc216=#Zo?P&2fLpD(7$)vYpr7rByi5 z60^W0#ic)y<})jy@c9V-_0yrq?=Lg-@|4Ao%X{l^hlPdVSUv>G^B$zZROJ&peB{MV zv-#G<`1qviy>C}fUgnEjX{N5rH9L5!elQwyQoVV|*49?D+yc!;F0Jhgwv69o~A5)J!C~ahv~bq44k>4!zkmE=}ir zUS}AVSa^DchlitNh_&$>so1euMA_>5>n?e%Jv~;%4mTSeLa{&Imqr>5GF`<>B|T6^ zq>QxTt~oh5nVZK?QSBqXPDQ_th)7~<>OaE$JVE_68JuL+ba2*61pm$g+po6C*D z`a@^6(hZUbw*?|HylGGQmxXh#;K73jH&c5!?1%Il;Opt1pi2!;CkE^WLo>&+@A)D~oVDiA2O-Z<;aI)7K9^ zmHfTE{Z7ewFN1@j5;2jHHf7JQox=Fs?{!pGe!&;6#3k)ANmwHvl1qPCqC(b@lo=cx z#PNS0c9i;Ah)Zd1ZZ0g8nlYCFXqVS&8Y{t50{vfoy6KEIV{3Eu@7jwi+CG(ilkCwi z&Gn7{%$alJBEEiFO)>6$9(MHmJ5FH-UP=~Bqfa$&g{(@BLL@$cGWe4yU!p8as5^uU zS(Qt#O?xoVX#RG7m8`=`?M9pPF?D?Y{CB8;g;DE#%ZowRQ(vRMeY=$Q7t+npa2ldOeCTLeDOc@e!r@H_5OYBO%pm=RG|IXac=$d`xWg2 zv>yn7@bkZ~{`~2$-~D;@`-2~@qAI*e4Vx6K(3u|cbc&bzY!5yS!LgC(SG>kWUE<2+ zqh0ahaNmZoAE^=Hiw}FU2V3Zol~O>YidQLeU8ulERWzV|%HzgPMINF3?TJJ7<%tvF z5Z+&x=yM{te3ydQk;szf7(|@;)+)C*;=H`P+~>W~adrKB_W3B*nX|EMee$8*W|;Qo zK))d}TxJEkRx9!Q{abeBDJdyme*Wap96F#F;!WgLAiW~&qJ3uXUKF+2oq@WNNF7Z? z`>nio#+FlZg=xRKc(KR(cH4|^aw)6Zym#=gzyI)B!uVEZ=szF!qpVVxvq{><<3n5Y z>Y_={#_LmxzJwg(bu40YwVScMoOUs~ZTIlu!p8D$vrl6rsI}}^ds{MZ->+q#urvBO zF+m+=>82pD8xWkV4S>9{HdIz#L?9OSaCtlWeuU5}9i9O z->t62Z|K?>BzaAT-4S+n110VY-4&aw=8f;KUAuOnbg{R^m%vC}WOQE`GBh^MD6*1% z$ZCNftzuMo*XmVrg&Jf$P~U(c-sRp&rGL&7VUV8V-bkvF)|`iG`CEW1{|iOC`P+6n-C8 z=Dc)2JVw2aGHXfYZ}ZzEg-3gfToS)AZ;Tv2Qano6bPOd~-xmZwFJ5G4OU(BZ1%s(S zc@;ii0_)iRvADRHH`?PeeYv%Uo4GlwUxbkHd zGHAyhhSm@`9nr^SUc`pdiuJC86D`KYEP?KNsAt~9#iq&`zQfizBiui*do^FtYwJ< z&Ntu75^`cFGw=E~U~%elt1eoY>ukrjeDC3pdoC_3S#_NKMb0hn!0_nl%k!+-MvbZK z-C{Doi2U11?B*O+`gllHfV$cfba+@4Cc8OGMlt92P4>xBH}E2}v$F%%nWS!Gg;et> zS>zjwJ%ue2#+sV0m5jM{>!J}haYe9Q^ZOB{OV66S{gzmEb(rA2h@73-L!L8&ksQPF z5Mvw>fD$JvtsNZR-veKsP!@peBz!ne+n6TpG&J!yg~x6AfDmpaF>ew0v4tmo6K#s&SFc?&H>doN zS?C;$tLyF(x3HW@&c7CPG)n4rIul?W4ZI#EWg`zNP^KzM*G2{II35wwf4{ha7xr23 z>S7UgBUvvhkb5hpMTX^>a;L> z8cPgy!>>Q{o~hALI)xm1T!!%eoncNXd7e@I;~52y#o&#NL0w(HY$tOkmz7f%>%!jm z!fYIi4gkLXGAJih!Lyjn6=Qw@+tW)^Q#119UXK8GdCF8>M|-r-QjNCvKx5sXKbA+i zbD2$ZdAI6ncYoCt13sQqMri;O+H49!|2lg|1^Z2--M=4~hSXcP*}B~p-lP6pjE3-A zTe7DcSFxJbO`J1bJU5FC_1s3=*oWvn`Dm8Iv#4izQGrj=oo`rgMTAVG9VaS47l8+O z$;hkQoYt<0> z?Lk=_smgKQbDDWxs+7<-1U`6>fx)S;7Pq4nH|H}h>CiM(l=JEvTPE^@Hws(go^jXc zR9RV>V#x8&;x&wZI>xt`D7vlz!vI9d$>n~2C<=l~2tZW$M3#sXM#xwN4MWP4-uxYJ z_W3f<&im*1+_0slojjd=GRzF6_Gb3 z$RQlNj!zB-43_M->}R;aqjF=C@wim=C3e`rEF>gV#u^&<`I3cV7ED(cdU?0t*$vFB zZ1uoG(3y};&S!F7<>F+a#2-)h9mpul56?Lv5emkCj@^C}P}fBp`J{oJojnV7GoT9h z<7p)qVNPK5>~r{&9@!>oRK=xk(IdKpoMTW6*k@z z!j5gXuYV$S2ePM(EM7A?W8A{e&(C)3*as(+w%(2Lg|&6IeU!npgK+aP#~#^;hv=%i ztwTdXRq3gz5zz&wtaSBGdM)|nlD>WW1`p~-$nlGx+K#;Sa2fB8I_K(2(Z6?ZB$!D1f9T=l>CGO~(7B%T92que&a7c#uLb#0 zW{h)Ku!<;b1@#0!*V+@eqh232SB>|~k5*)>RpJ1-J-xgrA9XaEo5Nd8|GCzO-}ore z^~`qD7ghlh?sGafilO3?c&_~_cg(YNT5!<5!eTTekax(LQ3at1)W8t039F>RE6ysY`o5XT9z6ltRH!5PD&N z#Fl=1?McDRbhj~9;dAq}^ZZ)M#|7f;^{V;75{0$lC%txf#H;vrQ((sIS%dBHFHv$A3i($lY3^Wvr#!pm2%!iL4kPH0=3bD9y~ z0~r$4W0nXuIdlaigde#u-*ck5jJZ!mp?^AtN}5no5)dCpP_!~P<(H~0r)Q{3LoKZC z96WZuecyjh9I9u1jBe4Jo}T9Pn$3bFH#JoOIT2OtrShe?xOR5dpex_1?LLAUf*T@^ zVo6ch2-7QxMV8UJZ37!En;8?E6mCAgl!g0@%We))xIv5`%3V4NC>x75!{X;M%8P2N@FVW zy4-^7xQR%?O`H!HN%!?%I?Q9Cu%Q+9XZ(Flj|t7?NrXHgI(zP1k? zux!~}OGtmrkYqxZD#{^aE?G@U%i9dTbP>Avi zx*5UU{@>0bPh#TGlHz%`T$>Yqn?ngz5srl)X8eU4jtvI#}>n|ftgfE4VLep+<4{`i>KqwD`OQ4e?|j3bmz>{L9)WpXkiCqU zRUHxW9%Xh0koD{3G$LV(oiyai^>y0rM(Mps*a+~m%CV448DZiX59Pn`VAP8sAWe+S zLUKh6%cu;o>H5>Z?Wz~b!iyey^zrW=*~wY&*`6%R7$1yP+1kbgmBs|{%WiX6zn?u> z1z%>x#TS6l{r`)(kUk`j2o1gRVUeIlEo_99{Y)!Jc6pc2FgE10*%ZMur<#|iklkOJ zw`VG&IW4l%_wgcrz(yX^O^s7Svu$oJ=o_k02pk>63pFtuV2*w>Y0CtHKu$jI=R21# zStF=tD(yGxUoUv>?-*~CLsU=*UsiJ;DPE)cME`FaGFx919 z5wE2jVNaCIbx&O`n-AN-y`ZlbX8C{$zH^H zN?&(*zFn_KfIFw(M&QcIN|cN{rn_4`wi@e?o_TWxnT}5i{n*X<#ZXD+hTW{CtDXCl zb(}`!XwA*s1uJ9yLdUFTA7<$So3*~~Y!{#O-j4Qiro)HL)Mhx{Rk$V&E4b@W?P+OM zpC8@F;`#Eo?Qb1pEA-YTi7B?H(?Yj@)+pLkE@R-7_3X}5twQ0-C~97m^|XCuuAHbC z9#nTaa2IOelpN+;$>&LHk!RL=zZVyGnBk5-9*uo4^9kYqL%MacI`du?4?^4&FE8><|3m7HBC%|> zeUWQmj2d4(rc#HQWE6J%MZ?i2jAqk7e7L+9zmXoK8(?@obIRG42WcnH zP~U)uL}0xKwK>Ao*Cb@xFlfJ79k}4L{IK%>o;NeP~6n7&GfSERzkgqBpLn3jgJ$$YEY8 zuNS~8`Yyae=5G|S@WdRQ#rBzFUw-7qH5;HZQa4cQ-uKfP#QNM0ije2}@gOsTD~d3` zd-w583u&??P6Cq919`~|uk~UVj^-1WJf@Szar*}4ya)=UpBjWKC``3ci9HwsBTwZs%xZI$R5An3i zRH{dv{0+>JcU@(zWjo%4`c~L%V5t-ZN4|H)y?)`2Q%A>*P-G?neqR&u!QLE&{P|_8 z?ePHxLt4qbLlMfo=7yOhQ+#*&L0|w@c;#Jl7nF55q*BEwgIFJ69BSmSF6ZlYY`tI9 zNQBRyN4UZ!adlS(7R2Mqe1ANmA1t9*V>?DcL>qhj&Rq;P8RbS7_ue9=_g@@#wHc1o4 z`P2mdYtDX&!D4FRdm;x{L|lt@%#jCd=If@KmaCa#+NQ3LCHRKQBosKmpvt6DB{H5+ zS6qmzZEsu4a$*WgozuIes2EznaARnllFSrZFe2!_+#}c!FA;jPa~<{8f46w# zszAVY%k?GCS^N0NGzXT1jEpkd4i0Fwm60|zD~9D(BkcGNybxO-#Ha=SiR{V#K{TOm!27Zl!fGA=*%Zr9?;X5 z$#cbBf6UhEn{oM3;eJ%Ncv#o|??YFBk1MPWekU+LQ+5$U&ZP0Dr8NfAb9lR&WmRws zLY`Uxu?rC~d`g9Fsi!`GG3I4R$b%^vGSQFvNX~6@=gCUZov1yCmoH!D=H?z?VDLl@ zgG!D@qa$a_drEtq^H`2~;osxAxf5q62Ee#WdP?uM7kBZeg&XuCZey1Ql6UxjY>^JN zS`c0J8aQ7TrLlp%w@`yi{92Mx5dD->MqqvZ$(Q%9ug@%3%Bs%9?RcZeo|L-5PDZe4 zXAn@>pXlK@{dbzE0&bf)CZ=c!*G+;{ZJmpK+{`+}>qK7q1Prd~`}c;% z#yM_radGE~=J83lP>Bj8-g!TZkk+Ukx3taVqM56TWJwmo!M-`PV$`yDkdjMBqUulv zN3DDi#pCmjcS;uOw}xa1J&EYKZSFLuclXchFU{8dVPDa7&=J)gpI_o<>)dqI`ilHg zU{T)@bksf$RMYLAJ{3Q^A%ib9H9l%}Jm}cC4?+!jJGwEO_3ryi1~%K0Mj^?cF2p(Q zSA3hq)FFN|j0OQMa>P>F=5uil3#wQwI3moVqVo`u>?4uff^!0qgWPVuyHCD?-S^9E z3)DwCEsb9Dx(6~geNJ|EDcTikj!>3RWj?+^Rp>s(#kFwf3Y2H)!)g;YHB+=e;MKiZ zO7x>!plW1FvrlKHJ|(`#Ck)6+yDp6iy9CY;A&kV2adg+I`z{k*dS|Icn&P%ca|)(q zvrR6&Ym@(D_OoR@8}HnRt_w<&Svj#GUh?ZFltS|4--nPVAO38^(Llmj=@jv(eDO8i zJfwc?l2b=pebee3f=gRYPd}yOwO1aSFO^6gX2mz>q{=+x_#@X--`0;9 zaSK%(Eh9UWh3u8K<4bYb#Oz8}a+$NM5N(D8Dq^#I{z@5Es8 zWAFab^v2aNO5V9`4tRChrHGFo5rv026^s(n&PDCY%Sit0qDfh-dJUW}Qmz%qzrE2V zF@^MqE6*Z3HzKD>{0$Az9|c`j9#Ah4R;EOguhw}wPgD&M7Ij1={Q4*SLYI&7Ac9jJ z&z!&e^|~)E;)qtLo7e2dzN;rJ_exMJ`@L`6s1MI%?}EGzUt~NEB&PeW%KePMV#P7-V-auO~B0? z8j+TkhJ+JEv%K=>iJt@HlPczY{`QX1U)4@XZ*VnwKT54JK5Qi^*7C4;;e&iLpYMF8 zJLT}~Vb?7@GPS&>IvdCM#_W^?AqUfV(Z}WDvK8Lup8cn;Kf)kfY*^x!79U^Jq^YlO8`juodjGyy z7OBu>s=c&SHhdR15067n=DlRllA5jhE_r`sUe&aBsQe8Z0mIs_m+?BELbvc-Ic`w+NRxh`*k~FJfI_()`Uw;5E{2&= z7dq~Oij{K0@JeL!%22X6GCt!c#pa%S$D%#{?V=hxmqf;nM}(hUDcYzVPt%9dJL^KZ zy0=}H$w_a6`oFM2p-HK+V4>4^!}I5RxeB&rlJ=NF!spq08#vX{|4;xmCKh? z(?#OrL-Cwo&vZKOOF zC*wke#C^ORBM8jFJJ{jk=X`Cr9^fG544M>RVo;l20HFn6Vu9 z(d&g3>n-{PXTtmDV~KGYbSO}+ddBozEdF{i858!aRa`x#4MWfj#l(x&+Z*RawsaY!h zaJSAPOHDe(r_ScC>amAbh5B}Sj$SPar}~hZ!S)w^;JQp=^gT^QN8W! zB>aRe7p7*t6p2p_+_Ps-%f}0%A`JhL`9Ra_AM+V?Om*2wHBtw}e`XuD5*u(lK` z1BA*}xxofoKr;5^!ml3_*#b^X2EypYandl2M4f>c@{CTd+sI zEc3{I2C`+v=DM%wz|vnocI+uv4cfK2I2x4U;VCyxy?|W)`INxcLfx-rBaax8#UHqI z-O!WMou+Ro*ZuWv2}S?!9Ca0yPVHplxA6%HP_ij4EiEsX z$5X$|e!SrPjJ8slp-?JXCI=Cv;1_s(|KbQJ33}A?-E6*_c;j*s4)xXqzdNvSb=gBE zE-nso!Fh#klT6kcx_m;xd2HT8HTh6(kfan?K@k zxDseMNz;#v+PhVTSNV)4dmJ4xf^y3FLl+OjT~9o+-UfH|s;%>SEZgS|*YNc81b9_e zRt8?pXJ>EURaakMZ{ME}K{gL^Ax1`95uf)yaF{yAkw5IlH!c?AR-*LAeC@}W&^cGm7fndXP+=y?Y`rXWQt?K@7%;ooyAl%uGB zo9{0WhPJx(Ld%wDfCK@4pc^^#Z{NJhB>gzJ^N(H7PLMgnr}<-TZSCt&Rt}E2*;z;s z=O-r%<%R-$+hVVckB>w934k0lO!23trMWIhhW_g69N2>@c>Dgnn^&p3DquV8v5t<; zrAv<@?l1}GFz(x@3y%mbZ_pr?g?!R6mHPE-;e)Z=KWP2obR9PMp=0dqZ{ub%NIl?0 z?d|t{*pD31G&WYZJIlsSt9qxqty%A*{p~y`M2^smS>!zNJ}eA6BTF}y8*jcpn{}1z z0Hlc_4?whR;gE5&GckE6K{JepNj~%#Kj^f9Buhj@WEqc#1Kj&k29$bG_wJdQ{T!_c z0x*+w7?^Bd9w=hm`F#7UBQ%$|c-7(q8Xx`1`sn(u{YRyaGf=`X{fI8+=NBK+yyo;d zLAtAE&w5LDY=2l>Ed2&`<-n^Wbnx!QtC`3?F~E+aMm5HpOzt^72~9iLraRH!g|}}-h0pKd+_H8tBN(&dz@S{wNVyQc?o#tk4Asr7p0(yX_c^*4Q(8 zxn}S!>9)MCJJ%HzH4_W=-L=1RM)!la}h@?+4Ox{*tQ7@B5KbmvrXa44)zQ;qV`1TS7v|%8kdzNCq1nhZO;69_X>}!~wrrKp5|_9jSuC*c zPwlt~IV_PxN*PfG;G=H{SJ7aHtD z%Pm_te^4UDZNHR4!zQ0k|3;EVqS*v2M^6to8<#3(9l z040QO+Pg}2cC%pu+OVHsKwyJ`dGF1Y*U4naNmbN6LnZvkl3sn28Lp4+tyV~O>E&?A zd5T@Q(3`Ain{G?@3%>R2Sy0`^Gq+9b`%cTVHO@mo-imk-kfG2IwdUp!(#0 z6Kd5A3_c>S69^ub>?sCD<@c(dK}rZ}Y~=0k0~V!UzL){lwYwmlffG_C5EYf@oOR*0 z6F-0Ubbn4wwVUh9gBauFnQ~4}4*CMq5ZY;a{^%XNoWD6iHx7C9?~996KAoGi##6ub z6>AuMxfu@R6oR2xdIoez&SI09rXZ|hJ3DiIDV`dE8zp&pT?WS>R338$@ZZb8ke|TF z$VjZirP&CgumuR?>(`%&k3%1@*dv0oSKkVfqi7H6Wvn$WZ(a_$N5DhKQ&`o0ZmR<{YHfX0fB)5lx!M#x-?*A}BHTt0kZ&cdtFa<6v2N4PJ}x$PdS+%JD-c4UYhDhl z-glTbL1D*fLSF=019TZc8$BBv8z9R#_s#c(3bS4;ot>RhwaDqDL?Evc=gwW!fKODu zEI>MF?%uoeyBhQfYiVf#bAjy!b<0fCYkr_ekrR0uLL)dO4-c+rBHG#7jpp9}je)@P zR2Z*MqE*-HxS){68=Z+w=B2MJ-}eUsO1^C)27*i`dPiHkZl3P==|@|ZV1mDRVtY7< zw{nCYQ{(ma@UtlBt=?Sku5j2CWVua8Q2m8>J+gwMk8xIyf0t=&W_|8*`Coc;(TRB%u=K4p z0bl)>KVQ6>P`%}A|Hj1=Cx6eTY<1ghh2bU_I8pEfLweAR97)UOp&djbu^WT2F>`Ur zO-QIU_3zFwCT3Lw2aFiHUoP27eCA~<8IFVIP@v+&&1ROC8TtjyMkfbhQX{O@>m&5? zBjE_yiU8$5Uf?8j0HXyU_0Tu4u)y`^SX#OSk%lT|9108EoUM&uAM9wsf7kZE>Tg@w z+e4!hCmUO~G8pT9Q_}@KNuNds9i5)pT%>}eccgu^53^KJD-faWeK@50E!eK@vh+?w z=vST@o9;?s2B z)EGMA<7;ZFs|!25VY(qLp9f=MlQ+mqDAeMVY#od3I-9II$12kdDVm|gz>tG{WcS$n zY{d30nhl8$knZ&QLGCzXKq1BW`OPz zt#|9zr^`Rm>yYA?fW;ab4o*&fwE+15+Ijd>Gco`R!^``-8^(q59$0CfxrR|@U|_HS zCW*s&rY2TY7~gDaYD!H{$5d-^13W=~*G;gwpO(p^73edXo-Rb}lXduH4gR9}wGU!d zp23mbyLK5CyZkV(QY|4l7!d`{fAIiX&h$g|uw>iQZWf+Kn{`i}lqC(@r zfq_X$Ne>E08i{IYpim_Pmr>51c`($U&%g^QO<-Uk#sEf%`k;lE!`|p}>N`)VJ_?S56EyRh`X}2xXtNzu$2d6AR(cKw`l6$j>wbwPK zTPTmo*zoex-d6{)Z8-64+iszNs?SIb4Gn#LeL2jj3xpoBfzkYOea3lf+xww;$o`9= z%q6lvi-A8OBo<2kbMyMtoL?Ytby)E;BvtbAoPjWPPRe4&(6~mZ zHM#eKK}V@2`p}j#a1p^wM&Qd)-99&aCAWdsLu`|VM49?pI3Y1{p*N?FzsAGJ^a`JJ8fI838UT>JgYEX>=j>NVwKcBDu=R~+95E28CmG1ar1Roz{O#m$( z=Fr-F>C&YHV(GUj7a_x9G1w74K0dfQH}`nuqaB=BNn;};5S2=S((s%9+<*ssZ=Pl;2nmFJq4=W(+-hO`7Vy%k05|(PVw{5)HZrFocc`uuq8*{g ztU!LcTYJ8%m}Mx7lNjhDt36FKHmT3o`XvWi;Nv7ZMv3Cqv|ov#>b74Uu1#@SS#2YZ zFnU)?9P-NDU74EyAKAd<@>aOh&9!RVFI+mjq$NjFK3?y^{zj`UaAzKeKnWRQU{KK8 z4uy@yD$u2c3Z|84hY8^c03B43?S_*vNE~Zj9kh{(u)u8=<7o_i>ucUkkme^@e0z|3DlUjq@w4n9R83uDP!lj zAsKbtmA)%q6f&c8;mw|!jxp#*z!#E^VlW6%|T!aPCdHAuAJ#{J$N=h$n z|5_p3!|tRBWCwqPSK&<*!piD-aB$ciMh1q49A{_e;7`C2M@L7&wn0|}rUqHl=H}+` z@Gt=R^t4UMhKC`?J66y8T8FbSTDts)$$ZW9wR;Z8xqdqKbSTsQuQRs2q>Evh9z{?%Vrk`Fe-N zHV2$rKvPkG3fo?}f7afK+g7;;ljiCCwmdfW@s{=mnaZ;Vn;WFHH4p5J*XgU%>pGv&WpHsUZJLsqK}WcKlJwf4el@pTv`vZv0{NAkHao zk=_4Qx@Z{nf3pD-{BRHkgNcrg269TtqwnwU2db173Z)(ZuE^}#;Qajj%7Jt^U4(IP zNQe!3pB(K>jkY#rWN63h=xEyDBvfqdA&~N`sHo6%)44fuuJLlfGZ=lc9=l2tnT|a~ zzln%I!ZalU3lfuU^T!L@j{Gb8fD%f3;6mcXW2lVeP)UGEl{oTYhLUASD^;>4Xm4f4O>^ z3{gAXpzZP9LeJG{P&yF)pzIdExTOSOprP?rGjY1D=K4Pp+dJg zJ?OW9h6CW$KXr;1LeWG5!6yCLvuEIqAck7i!hVCo1ErBh6n7oaf53+;V}*x@3LjLB zI38wUN%>M*S4I{?CRA?1u%S7wZ`pv21o3Q#3(p(98w} zEA7qo-0ffl#qGy96j_eVa|@;1Tg|=Txnc#xQM252MoZJyt{jB9@SQmR_}JL*W3lLW z#TMtr_wC!q$rWSn{wLq8Kk$qG^33q*oI(T9gHrFiYW?{V!19fa4G_K8tnsnGB%@{P zA_PcAE845HbkSUdczK@-w5NYKn37Yj1zR}T4+ZfK-nz11guO>Q@0XZV+cq{fCLU4b zu#|5V&Pjl30<@jXGW@9`Mck8M6lL32aNZ-!0v!pE+dsBo4`e7k-G_S#ClHZhZ~@dX z7fFzc)MMnK_{?1kD!i`s;OLxO_^g@h!M)AGb;ZfRt6_7KO zbWT-4v3&xnL9o*x3xn_i-xHvsD0!hki2W`t2$T3KO+PE+T4FM0wuR|0RCH@9+Snw& zGaLu!C?+@xKhO*+E{#cq|#VNLh zY~SP#!85`5rHY!G+e?UrmjzV){$-Ba?6=S=bI<4#MtP4Be-_$|1w=<+YH|>)_AqjO z2~4;^|6c+0(B2t~4Goww?8@+6ol1<6a38<@f9fF*EgIho)($FANi8&L9TG~4Q&>M$ zbK?}xotNr^QYAYaU(x6lt)N&nxaYo!i3!k+AQoZYwUL0}2ES>I7(K`b$O{$fWXI>* zepOT=C^e%-Gf7QA`&O=vLR>IiBl#8_WRtf8O)zsn-Bl{DCpceH1fvg1Uq^ksC z1iSDaX6eJipqi;Ncn9P}9x)1#Dpl3s0tDD31hoaJUcHYa`r-u17`z#o$1bK zX=r#65(2z?er5)>g063BlJz3YKDGEIE*VKy{1ThoKh6rhmvLRZwf8@9SmB;Ggf2-* zclbgG2w7rW|CB+Jm9Q@}Gc)6hNgc^s9I zmbU8QCBw&;1lf9hu9y!D!9ubJ%{4&q?|iNrX)SP?{F5lC9Z`rpPtNG{IMV}-9xRp%T3G_Q2WX+E_!vi>Y=KR(;Mue967+o@mG+j_u{m^f z?Ed$;EsD`zUPnK}*UVg=-E0VlZxzrjIomtGo~Rb3SK(VWASWdy)%5J?Q2Y~evSx~R zPw5#xKHVaxc;%lI5RNTIp{MA=aR$xk$jCnU(hh4+NL&(T=jK-J@tn4M0Zu+2`nM)+ z=iC0Ot|dl+x-!*4_zuAp0Bm(-rT)zv#St`!)ALLZin2AO#KgWR^O)iF3_#kk>o>Rt z1qrMB!Kji24h{~edPG*XyHEzpeY_x!B2Yw~E1!CQS3uuFv_S%ru0FNhDm&y>4`%;_ zQe*$vpy|&#upXETg|q@whVSXoNeQ5E`~rEf^{h^VBR1DnQ-hvXS6>|?LQ#n!U@U(w zfec9@OcII(Gs2CY_!QARj*c;sHdC%Eq|E?Y?3(gpnjYE^%$=B!00;=*j?&^udTD?J z{wo5lInUt@teaQZ7e!P{Q_}%@dVWDcSLrBt_~6h`?L6>7Fl;C$9%E%~dz|Z{$rHF%IO0a;P%oGk+*@C4#t+<2WCy-?jm~JL$&M&JS>nORtUGDR>9)a6Y z^EoNi<{UgP>ev$2{UwU!lm1(7hdE_;;@QUwenUImwyNujDk?9L+o(TxZ|7m59nvpZ z9VTeqiP4_`8KA>p`!MgX>N;ggo#p6Jh64vwv`&~f{ryjWNyHHe#x11Nfq{A z75foD?WSN7)H|x{>ii~Tu;+<5Xye_IH@3opNR=IDg2p(gV8D+y>Q9B*^<@4(?7e4H zRN1>eh*?w&7*L`LW`atTXe5Y$3CSQLNRE<|Sy2H20i^`AQ4x@wGZ;XG5|La)1xt}^ z2}u4wrwT*g`&(<)%v$%(e3<^!-AZxloU`})zE9fwHl3LCIzl|NBI-x|Ur-DFvq72; zwNzDAHQ%P|Uo5>gfBZLbP}JG<7n1Y8J(v*J%+!#_LKLsQwKsNXB%D-N50`X|&B<{) zd-m0K>{aojDuTLM55J>lW*0yC^|+NYyn_~T>v#b4dS8X?Mg61Yt=UNoJbZaTQHm?U z_5WOWg-;5!xQNJkpexvKm6Vj0UVphY_a3kj4p?BoF zC^s7$8y8o2P*8P!J*jPsD^PA7&bMM z9JG|u`n*_1_zfT_H4^n%n3>H?O|b^Ef#9xcJD;urM)-Mk7bhoY1(+2w85|_BYumhP z@!Y+1SCP*G2IC3!5dp=t3oHi{`Q5R*16)LPsaC3q@1Uh8>F(XT)(6;_yeN5Bn%w5r z{YVSAOUw^}^!Z_Jb)5b`JgZkf30}vgputu`b=8}dYT(PMzIbR7yu2q99ZC&1ijp|ACP=j zLPgV|;I-1*QdUl`FeT-KHEn2Uh_Tb=BtBV7g8V?~$Ptm72!lYw{oISD9Y;{Rv;Bis z$PyQ5>6KC4(6Ch*t#n{+w+%vZYJCoEa`0N|F6Yi87J28+`?fY4PvCSX$vGy_*qC{E zc%Y#ML%rs#*P8dM_oH|2(Mz%rwv>I&_`mpjXJOj zap#$if4-)rx;iQVK!xxbQyZK5n_4_rQ|Yl`Sp-TU#<~_?DQQ$o1!FJt%ZL~0moR2f zfxZ2gBbPgicgP6VICxdw55ij+5)u*?1_n9DW7K{m(_D2-qGc9ZVy6ljD)NgWqK^DX? z%lX2!Nlkh0?a5KX07WQHCmeu?h+Z7eY0OkU;r5`)m>o0a<@)XSheI0)DgWm&i_JO( z94`+qd1~LojPf*f)b< zMgGB$;o)!g`_s*a3E*UnaBlg2^-p&DzMm!shKI+8b$54@x8EN^(|bp+S%4J!xtz3P z#}0^v&(C@|baKrq)*nv2ypY2OsI6iF_1bZ8Xx7p6qJVoE}f_ulbR*A5{cIlE7< zFxRCWr4?P+9?)k*OSSqXR2pfdLzeBl%ON}pbQBhp6APEgYnoGHO6bbdKuc_f= zh-AVn0M0*kxN-Z`N;JW(!!?Lsf_~>z{BA$%J((sT!jdCg9lKgP?DkQ0Qdb4sxzj!# zprw)roRpSYlww?-_AzDp=FLTVA2h3yA+&uUV^5~z_fH!CVD2enWkt67by znvm6$n8s6dP599;%j+ykC}&=mv1S7sgp7k|!`M2Hr;2^?gC56F$k)%`KU8hOf(6$b zlicER6G@)Ad1TW_DuVYcprp!n8T^N@HRmXBc?~bGDX21%TveriDW|q%dtWAr+q~U} zXF`&b*N?zEUsd(U-XaAArT1?n+SBqJ=(ts-y$&ZF$peZY%wjI`a$Zz|C=CSxL!QAJO}Kt#?hg z#fky7aa%Zx^o608A{2~)<8pgEekKiZufF_Pi(BYrNYC3Y^$#^QkL_~F_4W1cX>=Ig zf(k){PMYa4SJ!NDl14fnXlmgOz2nwHk-HYdY3KO}H|r(U_+wohcdwYxA?|ew>hYS% z`!yD#1&?ja^U`09DRm7SoBPbOj3T4G+`M_Sc{YR)^;a*}X6#q4qH6X}g|@{7r;So6 zOn)x&X)QzDwB`mzJxF}AS3xC+js+@Jr2{kC9pQ}I(E15Oe~VR4;M?y#J&vVUyB&zU(sOm6bhR9z8qa;Ok95@K+vbvWo&}*Eut-;RAoMm?zS!x7xa++EN=+st%+qE+Qs4P8=X(1DlKF1{NgZ;czf{>fSR5i+GiJt&f zTvX8x64W0e9JYdic~L`=qZQ|})KR&G%YKVi`V!w`FKZp#HLNpqcAl+f6og!J(+B}9 zQ}HaOSuZjER4D4{aV=1~SM`^&V&H-U^e1Yb-%iN4MZ(Gt`C3;$BF8{-yjj$qB>!7` zzVLjoL6+^ux4}Emmp{PX#5&%6s_fr?<=}H?N`p;F(CQ*XUm!fYgMI_}r^Aw4#ry4r zBc3xYwIKB4_s`<%rY?`Jc10RPYpZ@3#v*#Y5M3!ll#!YLavAtv*1JwWOHAzV?RDIi zXZ!I5g8%v@fNl~P%sMDvA$;B{BsAh(g}xn7Bs!!qi`5yKna}`f(+l|xNPifeyy9Fs z!E~Wf*~w`*$FWbsLDn{w1q?uKNn~Q$|(g>J% z_v{02k(c@;06}1BcK;xgbvxfB`a4n7^CUf8eOMYhMDWRzCtsD6sHR1rc%mdRlh0Td z0DKS>e0R4w&sCU@@1O=7aQmH`;Lr%|Yx?q*{dy6uO?u%B{R|%WPA*EZHV;~8&#&({ z2)DMh2xNxDL_dBE*tLbbkVojgAf2&wot_*q?kq{dFo(AI{Ex3yP>Sx!QP%*3dPAF-zM!~#g_-H&(ym=x=TiIpI)?Ua_Fnh^5<~?%7i&wr1TJN`BKIIJG@`HRk_V`;Ge0AW< zxw3wv?@v!J5ATeE^p+YknW>0M}Z`c zs>AVQD20aC4xd@4f9%*`d+{8lywR_4`{C(IIZ!L0Kl?8i|3d!9$jE}r#p5IW3ue!< zI7dvU>cK-DK4e?8vMxE$WM~z90f)$fvxDQ6=lYuU@V^GID6%-QU zVQ2qyxvmz(0@t1Gc?Ptx!E-l|PnaG~aYWaRW%cj8cV zM>Hm26=h{%=SrRP&dv}#DW)5@QsW1<5iXPK@8(wY4ms6$PESp;?{naiajT&urY=km zeWl}$AP`gidzT3q8M}4A$Vgwy;`x_lBZWpwf>khs2c!!L3E9NdV~FcXi73L=o?0*$ z%hi&*z2ywrhjO99C0F+sh+>hp7Co*q{ecnLt57a@T7^)B&ueT;^K| zpxk|Ws()J5<7dmshtc@7jWSSX`Di?Nzu6S~0_ zGOzo1ezsy;Rn@+`&lWM!-$T0q4AqyE*RK$)yd+C0^q#(zZ^QlTg9GY~X-sz{X*8OF z=a4%88eYQc@S97;Jodxh1r4T8ZMNh6uYC8e@2>7+FI}N(BJ`8Az{MTORCw{1aZ5L( z8S}BOlf)0?Uo4P%Lp4~xS!ks^{w5@cl^e6>vVNT67ZDd%*ccEKvu#hlNJ~$R zKXCR8N=ZtvjDn@77cWzMdI0(gW>2nj ztG>nYVEVumPO_x=DMYjQCh{In1oNAnojv5Icsi-P zv{`uJ;>F}|e13@4<>~3^{qrTyS=#P$P5!3^ut}_3xl-Q2Q`P`;rS|a1h`ThmYpT;* z2@FlOno=r87SCLyt@XMLbagAr%FHY*s-xi&I_g}O{BD;B;a&lKU9ygcN9`*~I_jXb zl#~vyY4kgF5tmD3DeiES08NVkfVYbgt7ovCvlQC%t(oO1NsQ$k%+uvr{2+LqF&rK@Ynu9JB~Q_P!2RcVZ^)l1+f)gP|# zxBt3#{hjyqb|uJhg&tg6YSPPlyO_|HQ_9=yJy;x=o9#T}UwIu|Q`U{}&PvSo@iK(; zWntY0o8>D@T{Y?x5E%L}E$v5VSr9dft)Ti^n@YP7+eRiVqXn$V9jUfIz8;ILbhc&b zidt3OoEmVGtP^=kLxW>oupLOybB%YdZ5F9J*xi`N-|RK#wxb{XjZaICx8g;=A9{*ZC(6t7Jj~p=eBl~m$gliFD1t*GgCH2+f6#+Bn;95%QA+A z1O*X7_}H3;kt5UK1d2_ylv9vfTb;OskQEm_cU^l9$ow0JFh zphD`1AW`t*!K{lI+(sXiU7b2R9!_z3oS5kU z_mbMTrYIi^;T)t9a-(Hx{63YHmDADQW6{%aNE*o#MdA6wC!gr>0-DeZsxMBIM-1L?(uRaxHCQ$wO9 zrKK$y8K^rh(yi6iw7|=jF_Yz$Ki`J_FweEAPiBfc}U z($&9b%s*yA$r zu(LL$;pWYoczB`wLOXVJe@-8oH{Z2IY&x)X=Q;N&y!#!N<4v`Lzp?|;)UTf9O04gR{ISR zj1M36oPcmfT|>j-%ITdhQxmSKCxpN8@0Y^FP+C%wrZ5LvET*y8{5{1Oxc56dXNvlT zJbEo5SoVUw69utbCEgeFAktL?9IdSdi>X-Q5TbQ7RGcw0Q(EyAg`Zy6>lA{5f~Z+( zV&sTCzL#$Mlx0Oq``)m6tDp@B+5c&f{FArKMQkVD&r6?87C#92nWs!h?fr z>+46Z%S{w#Ibt%hvrBe7eA>iLH?Sp@ynfK7+0J}&>vUa8q>THTrPndR^oq4LH}m(0 zW=o2tmZ!&&-%8n6)8R7&||Gu4C6NE zoZV7VaeiT@Fqp1MIJEsy%(=#0vxhnNsKzdYO=m|mCGqt3SfSH2VmBok$k!@Z5w`6R z#DeZF8%I?g3W)kr7JcB+opf45vJrsyz{77p@VS~qpFOsPnfi7RgCGJXdM+#Xi==#h zcyaGuFZfBxlj9>gR*xT#nGX3o+i6-0cn7nqs;QA;puM~OX^T*qY?R}U8yz*fj}^8d z#D&A)rSy9x25;`~Sq2zVcP-13bnw}3K@NV(Zd+RP2Kh#Fas}|4JxNNlH+G^R!Ld}IZ^zQ(cFqN zb=`r>FGA6686o6^XTsQo^1@;@b#?Eq)N$H0E10Le;;}KzN_h1tN$ltd)9;{IC%`*Wn#m=+BZhE2^kG(k{^8*XWm6 zGP`{b#O2tTTan`WC0migBAV2r>m4EMNzv7Hw#POV*Ept~_t!vvItA_^$lCnESal%ZXT=#4z@HF@$DP#^C%oLF& zY7IhB>DIElULN+q(|Eq3LbuBFWD|Ap)crFE!nw2qsH#~fgzvPdjzCsR{makA(?vv9 zEm}832opvGE^m&{H51Z=-uq@-Q)Zns$T(Jp;CC`9kO{ zAD_9n_aPiw*UY7lY~Ioly2d@f72D?A;IumvO8w7u$^@`$z!I}_ZOuVOpYcHYo`SvT zt(~g=CjR3r|V{&wCbd#G#+uI*r zqUE+t3KHL@FvrQ+*H6)5TOJZ=n3X9arhZr^G8^V~&pSAteh;w%;bQm72Z-f=bolji z@fg+9cZGywMiY{^?F0c6$4EMAZ6&AC4m>kye8mIpr+ZDeM0q^tY@0w%G%#tXVrv0o%r8>v<7W-e7z{Xy^zHN%DnL-Gy7{UvRDhKwP}(&cYO?lk#uYj-1B+ufuzyAo|qwpC2<^H9$R~2W||8(H#9`7lSA@(WJ ziVUZxvM=a;$|q$sBc!$^Gx$c4j@IgQi@d!Gj1dUam>Tm8&T%0#WqY8Nm#OKqoE%E` z(EQ8$y3O;&(7Irb1p|!!mDRE!KE4++TIPgALweg$1l1GVe1NV?fLp)+X>R-Jl@G+D z)d<6A%XhE0EWR(w`R?n!$j@f zOrz&OdB91MC$}#PAZcttfMO>w*5j`Jr6?4Sb|Iu=x3I7%6$L>pT=GeXy<7U-6bpB3 zz+(w4rR#p-q4f9drmv;`aoOFw##n-N`fJ@_(Q?0G7KiSa{g^)8dG7jg0RbsuS zZs56{t$rB(clgqg^z%T%4&(uXR>J3eRq%Z{Z`G<*_pG!E#5m9W214n5s91_CW-3%K zM2-;jHI9wb*9GmdC$cg$A_Cb1#(j5oi3#y)l$(l#S<(<0A=ZR^gW+v6Gpgcy4ERRL znuiX({`oTtdyx|m;_PLmcJg$LEP%@ld~rL0MiEbm)p805zE`2au%+O2_k8`vybN;f z2=o^8(qXAjFCAW8GL2NZ@t#y$2}{o!df`}M^;PYDzUur6;tO{Ec4baQ^i?+aqc(cBeV{!T+QZ8jMWp-zu?rPW zq&$B7Dzikx!1ECFU#FW0LJ6qm`o-;2u~~@rfTv=(uVs9&l;=x=U3Z2f)SaRylg)Iw{r|EfB_Ot*h~Bun)V=U?Mc5Z2N6?yawlb{@k_BL z!Dg|6*Y8Hw&LHUnsv1|32H3$fo%QhHdO5jB3mRkkW1xyjwAfFXg_FI+rSvq?a4Eb$ zW~ReMzqgB?yRk`vwKP=@#thf!MT|adQ&L^8TgB;TWXxg8x`c0>STMGI&8-y>CBte% zj7Yy&uMTt)&e&a6`7SGJ)0%{5f2DYzj{Yw#?mo^B+CsyWAn1GNF$;3&lFR%h;zQz1Y zh@T@}&wK!W-@laBUVJ&;i2;F5XWoIa!wFAbU)U$t%;a2$AoIbENJlDW(Zl_?UVyqxXU|@5lqM!op`aVs@D%xW zi;8ZF4xLM?%E`@*pARGy1bkHqno5MH)pxVZzl_X`dIIc3?=e?Y&Dz9r3c`J;pB7); zPvu_g&gPq_RRhn)85b7`LqfWYuZzEp_sWsez%+`^^(8)fWN2;u&YA`Y33@p@OKJo^ zQz40%dkI|mM6=MP8mH5z_i}vi!KrIj=N%nav$5%zM?^*@`kIPtZ_y|RMSh8mf|e@@0grB zhZ)Rb9McJyO3Iz(<%KHBjf`DZDZ{nYghJ>h?vf^kWkgRoP zEoL1O1qa*uN1z}osmJk@yB1qmSp^pe@$=ssE+hZEIcw_EtQm~JZ~@y0x>HZtwqnu8 zHM>tch~Kgt2j+nd1bc;Agmm)xLKq&-=+QUekES#~^5Yh6|I)A@@ReX1PtJJ*WmI*l zrQ+9m3@wA{C*o=^nm&k)jYY*uO**~vnMsvt4f~cy>kM`k8Avm5hkg*;7|mUh4?^kj z#=@pKYu9%LgjDec+>mt}Xy3Pfc^1qi{B0sQH}okG!C53_ihg2!6_txrM(tge7a{F> zyujKqAiO+2UJc`DP-2*lSieL;26^7knH_uQ;_66@wP3BpFY}2z99VOqO@#q-G>Va~ zbCDs@BFBra*=;hKpIxrYgDo;KF0R}Id-@r(K{V~Ujl^%e569(Ec4_GAPoFOO_Wf%W zuZnrXU5g^{|A;dBpX?&jT7&VV}5q!UZ1^AlvpYa(mhPn zFJF*FXo1Y`bxY1V=$|c=hkazC2O8Me$2N3`y{t4DQv+|=leo~?) zNeKe4O&Aw%$w9C^2s<&Dcx~{r?q?>_c+Q|UV8fYaCiZ|HA7V>K%Yakd`XO!u)-eJ~ zDi=v(8{A%MF+JQ)P(6F)oi5OEiH8R@**d%j0G8pruEFt1*~n$V-Y>P*FZ{FNB^hHn zweEYyJa@J%$MjGC>1Ui~FVEhz%4qa@+2o-#n|csk-D%pL;~Q~biF0PWC$N8e3|us( zLF0(PM4KDS40iq5A77n6w`#-&Tv*{`Q{m9~WPjnb?EXrPCBJd7i5vndQdGU-7=!$2 zL;!3)Y$LlE{V|<*x)Eo^?R2mO%}hb;Z!?Nb*uVCDdR-=N*v!t~@E>~4|KTn+Day(V zvl|Y%!#kCjXk%>rnqt$YIG#HHvP5nR^bi=PhK!Vy%AzB^VtoN03;1Nw|8`xI z^s=jeHRq!OsjcppFmW+XVvU!oIaDtDSB3V{;~ONl7eT=gCFMl0ZyHwOWr{Iy-yBg^ z@}T#_RoB?dU&QYd%c?H1n`@=~17weljg5zIaPVp)8t#O4$%_}W|NWs+_b6?9czb2t zetbo^>dGD>!vUMG``HO}fF)F{vT*)BKkRgu%xwSagOPAyVkx_JNvyF5JAn@U;ad~O z`CkAq0hHH*f#b%d|6?fdKV^TlSIga?CJpnt}+960M$hVS)yeT5n1E#^IP-X5# z8)m3iVyz`Eadl8teTQ>KjlaZMWELZi4C4sL3StM;%S~=&?f&@DuyNQ;%!ab1|2(Im zWA7!;t*dt5Y=h*NHu`*=3TPC5)!GX`=!`Ae2hgr}j+Z|&{sKnuxT{0HG^@|oQbYh~ z@}D|-bor9`5SRWiL+=g~cA!8h)5@(Jz!LI;PNrc_DjO91fR zzrSuf@xDTd>w8DD{Bf_%V=cY$NUiwZ*RKzyH@e)p(#I@S&#_NxsLJo%9aWc^IsPX7 z#L0|e>c>qut@`_@2eF69O@EnQ&O&^cOE3Y^+gyO4q>cUJ-gY+hoYNDP({~m+?pI;pm?nTdE|>CF(_!oF9gaQ386zkh>J zZP3DkGk^NP%-hQ{mKCNp4T^beUV`K_zQ$TnWWa&l@LQOE0_4#;FpU*VaEh=9_QhJdki0=Ui|f~X zj;~Bw@UQtC|7WJhY?$C-2pIl%XLI5)h)w%_pef+QFgWTk%$iL9D89S^)FA;vLTM8; z4z3(s93a%;p`rNa&rK`QxT&#Hx*D8u7&pgLqi?S|?0mC}FEqV5<^MW)+-*VDHop}v z>K|2-?;kdGatblnfs&R<#7Pxzvb=Kx&MGbm+lOuhW{;o?=A2<60fDP84gE_UfEL1S zg&1E9@!8pX;-dJFEfWR*5vvcec_hlpcu(c6ds}#|vF=@`D+cLJTqoP`iVAR*13pNn z4$W^aS#RUU&oM2j*-|f6q@v_kv}1|QO|rd%gH2lj*@n{~a7OavLob<0`!;r-b^3Th zdEErkPO&TTdm9=S_Yg5N*z<57;zJX{@Vrf}P4@FD-^z`vy>Y zIqCj=Lf}du8%d|)9KR_Pwj~y@L$ota*#rDCq;3(iC@Z{5R|IMV+w!j&zC0FJIJU!L z8jk>g$gsw#Q`;X8_wBa#JbS2Cq`HUQ+_AUq$oUHxlCN6m=DAdHA5U#*Zhrot;KjgZ zBZJ3viJ#5xU^74S?{7K{6YOE=+TDpQwHXN`kMr|eEOElTVjt{ee2+vvS)4N>6}k<@ z>Q?KL`n$UcDDw}vqLm@Q!o|Z=@6TGN$&cv~a6ofIL+1u{YrH~CN(c9yG_6LDwjxpIgud)St%dQ!v;B&Vuk&7te3dmP{`@_JKpSXATt zLch2ao?D`dFZP)U(iRp&t_Px5S1TXsc8?6deEAaJS2ye1`ya=ke9GyLLX+oMA1zq~ ztU_R~@`rm+SPU;*ySV;i9{o0)b#lt^+g}}X3z_MEp7?;gmHxXgdIuzT1cxqka&|7~ z4dMWM6DeW$V(b_zgcDu(tt=abkSnHIMg8nYWiJH9@%Sh$OwG6x&+>)9O3~;wgQ694 z!b^CK6T+%;R=i<9>NynS8j=|ow`0{JtsriOJ`PU@6k**vEEyKdR=c^S>hY2-b%C}= z7sX^JP6BoT1D6@r_3A*Rc8kEVg7aO+A#AQa5>P+8BD(J7+eHQIs&8Hh*I3=kVyS;_ z#juO{UN8wtjIE5&pb#r|VpR`<3l`KTo%cW}xUa)??c5VLnah$MC$Rzlh z>v1;jK3e~hQ*${>J?;-!Thf?s)r@kBRfh60L`_Xbw4^y7CZe4wnlE6ea)4xH{2e5U z&@^DSOH9zyTRM04u$t%%&RwfXJlx!}_MM8Ctj+h1KD+Ya0iR30K5q>5JAoI%#I@p7 zN5B{j!#?#xy64_pa=8H_xbol*VjsW&ZGXn%%f&ehZhM+9Qi@N9P|~!pgLuvSK7$9K z)+ol=OK-K(1@7b}qw_^IS(E=a)Fo#e=Ny(F$?I$+9`(sN3L0!=ArqMyMMj3lEvu$#6239?DC{|OLOEw9xe$T2hYx90xZSe9vOQr0|`3jrj6~Cr_Pv8%Un&B;GH%H5N{#ms9E< z&Jw+~=5&r`OYZ`gUW%ybQH3WDVF?%QVhJpv_HGcBO4QFEi+vF%?2EAFF`s0+@Hj-EK#^a{o`;u*ns1kr!}_(}P#$*F~Jj8D|GPDSM>ZvJqg?1W{~h0a+k z0Tvv)e8n)m`u6<+`9r1IoC>y-PF&(xfMQtWEeus^;8JrWD%x= zlI&nLqg^6K)EUh{#<9;BmM!{7)1<;t62jHSZ(aMP<*-X92!qsyHZvnV!oW%_f@VM0 zHV=J${THuY(5*nNP*nTsOfo`X6rzsp2PQa^N@;oh_iy+O_j#2M&~L$CO%;RY{nN-3 zG&a*r_eO|2e=8{W%In|y@1F>I0{_g6qBD>rx;k6l$;nC7F~cvg6~4T_OuUza4iHuh zqeP+`PB18p7^{JV5PMyL`$)o6niCdtA$0O%b{MfvVB(u0NzN`uPq<)4bCT?kdep$c z`{%&Nxe9Pu-+gaG8Npth>oi$t&W&0c0IjRB#lx2J%56N;}sJO)Y@CI_7J4 zTlz;VIxun8TKufv*+fxl{iTU+EGF7&Ajb{aT0~a443?g@JFXN{Yo?@MeK$6orsHfz zG+;02JWMKIyx0#9;yXcj`pyhLfSoeOre8gGeizkwQI= z0|;q*JcZkC5Hj!F$tE?Pj%k8Xj581|1AVuB; zau^Vbi4=5=46Bb~3M_kP`_hgnuPYqB*VGK`g=~s)4xwf@*}_tvqxkguW|+eY5k(7< z2012}vOf)GQ3GKRo3Q@X&F!@dns*K&d6PXqzEOI+>Kq3X5)!boSQ#Rvn@gEkQ{NBvEi{kMb)6z{>SA-ZZ2Kyzq^DY=To!-&uP^rjy@GN_O9Q*_}o zMx!K2Il1W}#80c0szG~73JEdki^%-i*Y^%dXK)FVbv~X*1NXBuocUf;cki=tw7a60 zsFH3!o~0X{TVXEblR3-@Wkuf31w^b37Hq2-Ub!vTLGqW0Cs+ckzxmt~eYJ8v@nVM# z&Bc<+lw6NBo_BRdK$a!En1Rz7qj$3L6{qoeiLmE8^@g?et9|wkn)gd$-fH$s`zEj$ zPe%#|p%BzEZY>j^Bet^twVeMmr~m@0>X6Xt%-0|iGa+AJ9dQVj;@HAR^;f7(E zu?&7>{Gz?`y5|_Y+?A#jq^%;uWg z98c#vU3m;!;C|(>`7d!f|4CEs0x z1;o*jk*+Q?t2lRK{}^@2UMxeO?e$vW(PXDZ`8!m zDIV*5-s5-AAJmp#zdXrA*Jct(qlte`x8@PLTos60f3i_&g{LT{W9?y_^`Pv|24l}J z<>t-t@bdp(y8gc`UFYpfn>Aw~sGiZegr56|6i(U5{&w~c3frD$pW|Ik%9M>yND$k- zy#wn7+BOyy)8{@A#%X0u&R^TjNdTk1Px1ha5(WoLC@kXR;p16NsxN`0WfcpHCLs_A zvg0%mv+1`E$eX$ntczNW^N+Pu+y=z=Z1$stm0-fESAgtH$c(6+j2Av3165+xx<`2f zkT>#rAN<7ObMRDs-*BD6DP{lCpqu3KywAmTQ%I#o_fTUsMJUiLzCnS8U_XGXtQHPW zpvT_)!|_3Kd_38@R+2>#17p+)g{U`UX-GArC5SV`)!w6_-ptou)*=2dgjd1u@fn&Q z&74?Rz;YI4Z5k1x?cBxKF)kB+=Kn9M)^T|#fBkEYzl3@6N;oPMwRRV}hwoK;*W8e{bjLBqt(zp zhfEX+&V*;rc8YJ-#>PakC;`Wvy zsp(IDqqK-p>fSzS!EW`m@#!wR2O1U`)=z=oR41Wi4-+{D=))(b|DTD}6Z3Dq^PT~X zW0B_Nl<~XsZrzC&0FJWd3ny>_`xfg!AUHxGm^p3%C`)H+Kx0-i>bhnO}Y&o+qW_w6f=oNSEz zCHI<9w7=?w?&HE+^r?ine(~JdL@HeRx$4YYsG$TB+;n?Lm1F$(pwkQCI#Ri>-@+*X zL`&4ecP6<37Tl<+t&i}3!mWPkZ2q{u+IIiad7PX_$Q&T-P&=~C`fr&8(Zc=#%?XYD z3{HM0pq>cwpeuL&d?GzZh^`XHL;m;VLqA80A@m7XU;Xb9h^q;$H)P%#9D^=-$fvO@ z!s=r=h1jaVPhlr)%rtG3tc~JI(*=TZ1n$u_#or;dpV8G9y^Fnx*%Fh~7E>huKfThL zJjqa5rw`HzG|qy2_)~%D=C|&q>)cOoU#yz{jwSOq6+01%qu7|3WL0L^%(#g2z`v$_}jCCbWI~hsQv6@r{^g#~zSCoJPl> z`PNc*McCkarjH!c&~Tof^5inHvg!p7H`3e0w&*HuPtUn|%!?IeHwq}VI7$FFbZ7Om zxa`VRs%Ct%v%~Fb@p)ZVXnQW)W)f3_qVGcQ^F`TVHSaA;&+ID8S&{x1dnYX^xt*62 z&aZ-Md0(t&=ms_nT3K7ysW#>r8HfVt4qRT^Vdn_P2TnAgG`6+%54!NB*9GT6d`|C3u0?`ISBl= z_7X2$9bePnrOQ#7+d9iy@{qlkf%;A^(jZ+vo45OqW+*H6&a<*_&?vz4G&uAofVa-e z=V@ce?)kGXBJ1`n9af%bU?=yFD9dw=Cs3?Mzkd6+=<5Dci8wwAAyaP)w#Pl`IBI`F zrG_CS|6~?YL!oT%f?q;W;z*fk9lNCFx_B{>liS52F>1ILt{TSBMp4wi(6Pd-SUd2C z_-K|`MExMsi3c0+Yp@|*KKCw2aP#PisFGi z497#(K7m%T*(|6Thw^?ns^u8Mk{uIuAwj$bb=89*4M!eSYRIZ3^J{mH9+DYeE!k?8 z8bO9?+ag+3?|EwYm3b1}0k`*j+;w@CRV#5BXXcQ+%T8|=hY19ikP$gG<>lH0O<8Yf z9CGrzM#eW;QMW&@?{-Xc4WHj&GakZw7`KL!eq41MZ<-qvr<--V)3qg+KCyBrCX)YA zMp>S3>Z(KnERq*(YN?`krE39N+gP^ty+!?@KIe6v<)?7>CRyrTx66fXs>v70g-7tT zU=YIom9W?T)XWT6mGSu%hFg0n1rh*^N5l7WTj7o-d(!+oeNTO>*vA1wbKnc@(2M92 z5Kz2wZcW;G&cpf~uy_RmGshS|4s|n~+AJAVOwWU8rgIYFnQwyX>6=sfNCZ9gjbgR^ zW}xc_5#pLUO=;9$Pv=Dy)j;;1ADOXXY1o2FNJ|rO7|Gi}H*U?nT#RTbTM6A`$Lv17 zm{&SNhAL3zZ{t?rvC;zIkh1ms)YX!^UqBdA_Jkn}(e$tVV7GOhEi7GEj<>|?oaj+z zN17Hmvb13qh_yac&6|A@jXzFz)D}z37RmLia$~;TF+0$It5$mNiiXqrrGt#ur=o4L z&)HAeZCzRfFa(>z$q<9R^z%%b2|?dLi27^T$SrNEO(6Rm4z06!Uq2@<0RXQpu*b|JA{A zbhXV&sZZ0PqEHGgk%WG{`+G3Fz0jfHm|fz;Msj5(U1y96NiP_mRwuzpYhX~w8!`ri zkK2}vLAe5za4F|L4@8w-+`C>}ynY5q%oraec*X4l-Q%xllvF2a~rE=E7T7{|d7lMaYpj{ww(fw{>2;~7^ z*wyq8hOD|G8ndT)&#&3j*9qc1^r-V^&r01g%kwVu!wenDB*JD$#7TMjuqNIUXZcKApFix`IeIG zl}=Ns`G)yPNVzE5#rvz1d#!>lFHc5&*Y4dZ?jzRS3ZtK)Oj|I2e&=)hX5e*79=LP9 zkHjMivC(jydE#dr25!1hB^*a>`5+&Eo1@s~_!4^qE1X&T=JLuqA~I%j*x2-d z9wLWCv;2^qqQX)UecYBSY^lO^Gq*-I-e5I-pOENn1Tz_KZ^!bXr6t^Up1 zD)-zfaQr}HsG*f+2U;qN&y;QP^^qpgHH}M*4Q_WFPD_oAeM^Zk5)reBe6fpCwy$A5 zzogEygaj(xvxz7KRIDAPRn&C5UwRwCei89!Z#EAZg|KXU%30IuX+%O4A{R(;3hxF8 zVRzcxR*EQZNbS@Sh^l(lMQWyBCXPLNQy3>HdQU zcNc2Q9ab}@u^KoW(v3VUgbRi|f zD0RqkDz#D;zWF+TB0(^9DB452dsh?uYAM}WAFKn;Im%n2dv7ePXe(VBr+er|3WkC0I&DmtDEA= za75h{YeSx(Zryif;m`*Sc7o**47re0ouYL{1X)c*#Vm&U5F#~^7l_waQ3&6^HWf(; z2!~8E;1xGrG*nys^bCy>Q$0uc))9)dqdD<#VUGimr_zC#qV59M@eLC4QhOG1Tp>VR zR-dCE>7z>Cri#i+@5!P1F=0b*#LY?XW^iyPjA_Rw;7y8o)Y+6XQu32v-<{t2V6O=j zrL5RvtgRi^v*$k+>Fl0iX$gs~BaNXFXJ!=?mIj?jQf{?&* zHVG#NVpC5Xk;zc#OFh|6$Xam{ERHE@V^2)Cgwtex_?=UIuTG;SlJ)qx^AM?RJJi7_ z?FHzAU?PQZFb6xoMnVgAGttC~FMoNX8(h^1s{vRIuaVrg_}JL7Dt|A&U?K~_pu5Mu z&20Hacm_#a5~79Y3A z&}l6)bK9n;M#Np(2vZV%vfZ4xuVdfhH2LMXqJ!Z050d}?aT5Vi^na0-Vm6oI-?&6t znf#~jfAJOQ$1!He2N4q9@9jMe`*PvZqF<_pcd2x+S(kJJa(^6|`a}@H({1V9GxU2~SE1Sok8?#s z-cP82YBi$tAdE;Eg}NLYu*1D?1;rBJNzJ2%9>CO0T2R3E%Nx~j?I53Sh}bDf;w*JS z{||E=!}a{69spdUQ!lqeHw7tG2;_V+T-Fii39Hi&Np63Is4AFsSqfkk z2jk_3_}DTB=wGw5wPp)AJ6oCv6su>H(L^6ZX={1zwUE7upoeC}WAUT6XWgt-qzhxY1+h+LN$?nU8H2Pi@L8?MU{z zYtFOB{%n5Nm8A+LLh+SlW%nZ@GA_Mm*q41BkT;3bcV5{&xDtX*cr(OX#2cTt2Zw`y zF0J#oKcf{RQm?zBo(LU*F-&sD^^7pgqG53zIw_+FkTl=wlhxHd{C`FWbBHczkG3i9ze7m=HB>*wdv!od~NJ)c9OZ?GsI?6(?m_p-KEtB4|JG* z!vO?Rezbv)2OM`TTXx&ePn)4|KTR8uNAnhV52yuWOniIz$xost%iYunjweb{E|{wZDMP?Q&^Y=vQf`IbuLL`jxTykD}AXB ziSXJ((F<6e9ZCx)op{4pU8K21>y#879BA`96n}_nXldP7U9yx@g3h)U`7yFwNSldd zF1iJ-j7KR34UTwAHtc9NbyM*P0^I&OP3*$1b4%wv&Qi^t>^?EB+pV$%+$oH|$4a#- z3^z-gc1!qC_?}-kSkSJ_`Y)Rj2j3?QY(U=N=5V_>w&UE4j-^rDwnt~I?dd;NUlGeX z;WZ5r1*u~84EaegWtJOOfP*cZ8cKv3(#E&1udiNw92-}6_|8NZ?-pW*Nm#{!ZU%3_ zxcB1kde~U#T?&V}=#orqI z;M~T8IE`i7|MoHD2@|;J)R4h+?N|5p%Lf{NmU>Sp)aHvF8A>IKHQBh3gp?Yp=*}aI zu*>=kaSxGhHbIk{rtwU)F95;6Q;bM3X+iE~JFy;MP%={00c4#Gsbu8TkLZhgnH~{X zYZ2A(x}jdII9nHv=Q#qG9|2-C7)}gl7GjH@w(z7^Go~Iz0vH?FI$px^r>H&8*!4Tr z>qf3+#Q(4#j<~jemofsm_+tDfM@ z$LTVjjMFrntmAdR7#f0%*QYWi`<)smoL*o5#Yqz5ZA%q&k0S zTc}qIVa9vP@vB^lJ}eoeqqywR^i0REN7~VBgh{gcG~tXO*3j@r3g2OyRq5k1b_xVnQ+S4x!;06vbEQ ze2ZzuoQOfiL9k|vEWWWMQG-J*C%|b^d2iO6Ys&eEeZm}#(BHw~4X4Inqf*FRF&NV3mCObbe z^UJ3@GAoLQOr?$5?*jA= zv5w~i@7EJ%X&FRdx|O9qHz%;uO^kRf=%LHTH+?ecWXNx7tXeCYpv-B^zOTADTsm}8 ze|loVLv*K}h`R{8qppvr#<62n1)k&J;L>^rbSr{k*&wCNt^h;hPTH|#>sv#6D84J+*0r$s9{ zlZ=qg(>pbfA7@>99pVKX^l(Qy)RX+%KwXjt&E>Z&*2SZ4kX8@zS=g6Dd%26={@ZRC z+g+tJp4sN5EDux3Z~l)QYwKZn%frpiZddRmRdqGZX10)YV#yJ1!+g=_3g31#eh71Y z#DZ0II(jEiLfR;(Lh)fo>>4yj!n$$uF5xFVe+E{CwByJ_)L%d+6upqGr;ikLFRuu& zgdQz~)8x}U*PjE1`76;mJ`M&NHxQPEw+TfJ)D3YKE2I<{UcNAgenXH+Wj)I;=T^JZ zqyIzOd&g7R$N%HnrL;8=2^B)h$cXNAi- zxD337?PYB7>xrRP%yzYLxPF)%L`~X zr|UhYgR%!ErW7}ZT)nJs)e&I!X2Z?~PxUsP{q9@%KF8Qzb?T9@PWtV&88ZOTx38dw;8iAIULAIpZN?1I6!CU40b~51`R@MWIVqXX(-(>p)okWl;BHB# zeC=I?p1Tw6M|XEO{8_rXSJi=QAhP~R<_HpRliBmGovirqK_AEPj~yK9r5mGG<)#R( zVSE?HWu-Zh4Oe2z!%-Y`U=Z*X5rGqt^Ob+5joyeW=KMt6PIGJy;d|kNy}H0C_X`^P zo#5fWMp0UlBE+J@$m^u1jJCJ8Bj}-vh${fz5tT%1!luKkl9bj`-qdF@FaNwN({D_( zpFUm9!=s2qi5?=91l<)>?SKD$k(wS8Lq|%*En1YK51}Z875GDV8WQxeBfO!3XiV^0 z?xZ$Ul$Y@106VdsE^EV`FBB9KqOPwm538J?$?$4+cD7~9D5me_ zcC<#yxm5TkL1_4ajvYc0VpWNEsFM6crt`nm=hQgWvUXm4MdO{%{5arcKTlRDOJ4|q z0oJAW<_-odso6U2P(Rd-?iT^^$EC!Qy(WJ4BLrGqy%I&HsOa1Uonl2)lUj8R4Y=&6 z&FH=F+{q&6R4E@x6dLHp2O{}>WSj(CiqwiHxuL^>NXi{+XldbII9GF*xVXKi=ZB}O z9`RLOD~>64+G|>D5p|9AS@U=3)14%cEGT-W(v$(ZX5-w#I9O3 z@*t2+gl%UiEM2y2`r`sc1cYY}1EJ#-GpeEYm?1+;BF(nJ6DA?#e3A-#M<_e*ETcT5N(+}=Kz2IQ%&Bjo-QsU17#N{aCg7} zHq(5 zo=^ntA428Ypv3v`YQFF&VH{E)FE)7IJh=j9Z}_hs#jCeqeOTNC1lPB;pV(MK2P+6( zH&<5*`OYY1>PL@mT!j3B?)Z>AuHk&Qp~{8^OP&`9J;t9^Un}p}VWe6{HPFWj$n0fAq~xfP7%tw}*iWX&g&>cW2W;f<}Cj_x#ys zxpEfgeB$U!Awr0Q4_gc-nU}Y0*|MDG@37zfZIu6?Q>ljRy*NeO=Zz#QKHl%_!@aFO*atp3 z>b6Gu(mG=j3-M#u&mwyt2BD43vibGm6NF(vO|2CJTW0=H651h`Rxjyx_1%9CDneZI zQo>n$ubpV6&mg50EDhBte#;6jQ(w}oLL%L0F2zXN*3}-peiIpqhJ!~{w(VJiKg9Wr z1K~!4Jb4|Ah||T=`TduCeqOpogC7{QH25FNN8&y)9$elfG+nD_w>}U;;;Ed1@to<= zsA=9E#2?nnzmz~vssC#c)}Tp?v{>L5Q^p-8Z-(jV!k4RWzvLj*{>KlAEA^{`=HZp1 zw6LsiI6+xsGh<+c?jzpmH<$sr}|HC>!Tq=VD z-0M#$k(l3tcIVu9sk|S}bSW%uR*aTvE(q679 z*PudEeSQIhiWV#P`SCTlfoI{|7Pl`E1mi9MmFj{83zCSm9Q5Z;6||C)ce}ajD3{b^_PMPMKJ;#O=oo<38+nhK1Hd?V@EV8U)~(w!`jCBvY6)tjZLk6WI|} ze=_iXgDsQ@eVb_2;{G^Cg~+ly+%%1Ag6GrwUAym4&3K%n@A&=bxGlE7+y3ZfiS3dO zoz;TB-htfh#Iv!E_rRvTp{OnmM; z_&Ixfm(4$qh7Bu}(9%PabaI}0aeu$P*UbV@s~`gK^kUmelZ_#(aG->(6-TwQYbd9* zZT%P1xcjMYZ0Z7}gM4>x-#*&bsmq;cA^9A)H}#&)Q0SdImP58YSzd+#>F+?JB*$R$ zOw83VF(cgY&KI?6jS{LT}%G+-aeL&1K=$s0yY}>>Fdr+eV9719q|n zUTHj&(|q7j@k6PaxQe-}mpftqoYPPzi9LY-gGD`IqT)K)S+DJy&y`xjDAXh+Aw^)FO8bfy{xGFX5|4yf;BY>TT8^QdCvakMOg7De_AJD@wD2Wdtfzo zG3&X+mwrU)v*M5roDJL0DZLCUR*Lerwlle(UfQo|{Qm9RYaFFlRl|moYR=Wr(E}jC z(U@K1F&Ir#o{*zY4^wJyifp1u$Zv*y2Th$&9Fa^wV9~_dlnQ{sW`ADfMO|Z|0ZYC5i*QJ(>mzEioTDlQ4 zaZl#O3*`N2ZZa;FtX49W+p3>Ua^DWh(z5{z(QqFBAsUjP@d{M)$5Ki$T+Q?sW}95< zoI~JzaVkz*LkX0q>dP-0q{h=Oz5MG-5Tj|3TDHPQVS*$5gt(+66?(&bM^n>HqN3ly z_?8Q=rJ!k<)RWxwNbedg(BHq5qEvm9?SPtq51pS^K;YG`gsgB6=tL0$--t#`8#-QW6dF3BPD4FHfN=oOQcs&iIe2}x@C>m|Dt70!FLE{>cYhQ=F{jxZ7 zbmz&6n_*#kuaFg~bY%NBN~T)FCcN8bn^uHb=?S1tmnD|d`DhZk$$g$lRzzCG@C}{_ z5du3v!Ag8VzwUcr0W6%f5*!TXCr)7btL|rkW&~$HUj6QN>W^~U2gk@n078!EXxbw` zRE1)Fk(wLvR+?!_s_V(yAA68cdNywQ$e^2kxFf??t z(4;AcTe==1R3QP0(_PR7S|QOlhh|+LY{AijhenI)7O~Ni|EgaG&dAJRFXCm16Drl^ ze2J5SI&$bPDwDB_bkW_XiBxLMR1XH64x4z?l=ta< zeeP~Qi<7we=sU8-``X@*-X4aDy}G)3+oj2qfAhadF`vdU(km=)BzxnE?I8Vx`X}rZ zs6I5(+xk``g;}WDUoofE$jvKY5%w-ntjtW4R@Es@FnI8AU^s9%yR%f@iKFuGo3k!R zf7KB_m8i?P?)@mo~z}3mIh1)VF zA%-k+%2lnk^xEDpPI$nP%ig|m(L~!`hMc(&nUb6|^s0V$Z{N9drN7wh5>x`2O+Edt z3X?xHV9|I%By}^h+Bi#4cB~_|i-iVu6q{WIp+CY3p&hiS0b!l+3NHN<4Y$YNVRCE; ze;vI>`8OlCj!>*wxu73^lEiX}$b#pOAxKtvH%LC+Kiq+VLsy~GDUs%ayg zSNZguJ>4dGvxq?j=gsB6;ftJVv`)?gC0jW{NXbC=(xh7_z)wpcg5&he-*XC8LomC~ zbnmpAd}J?3?8dYZh%V2U#@JKGM=;Zz{%)-kD1{G={Qr4@L`MaL zp-}nr;3%-C?Yno=;<12 zh|ZMpKL)cx{7TC8zv#oL(~JML#jO9j4I3fg%(&FewP6Da$oF`s^Usa3<+8yW*dxXs z#39zU4As}yqv5e_$&HJut-xUcD(yI7;H!BBT>}z2?A9^up1;u$SGRP83+o*_w&L?w z*x*-=k-D(Mh}<;XWz-HTxw~) z9Mo1ZdQP_9;`YU56$6eSq60s7(2C$N>Tc*A(ept191!^0rXvurpe@agUq^wf@&M{c z!ywEk7cR_=Fd!l&x@GJ?UpUihnBYFIlGeMKIol)|GmeX(%ZDT8WqDwS(opKNXU}k0 zkU{UfPBQOArvqZYT)3`HpbB~#?!Yg|gpVW4q&Nl83lMyGZSCg$nYsvUqTIdfZwgB$ z*6YO`?+I=I=U^4<9liswKj>#6skgs>tmPm}-w%eD6akX2%W;=qf0om1+c1i)HRvMB zJ3_*c(fX>SM4LoXO(U|9;3ZXMdP&hbAGQW(3|KUHjM#pJy62dj%Rhx}B1D_GcW)6K zsbEo!D!mxM7jFI;6X`%1!$is)?AhfIm`wQkam8qs#HN9crHumpup~^R1TmTeIbdq4 zJ&B^79;1Vmcv)tI%@6Mf7)!jSWpNiFtRn8d~+QD3V`v%!#4`jKig|O1HMrw0Pm2!6=3xZ61*9p9Ym9tiwg_)Af^aG z=8(TpkrgpjgTaK8B%yGhxN&JM>#|ceI`wg`p zRsBsM6;K)Pv^NjgncE{GN_p#%Z&$ZF1(rP7o(ne|c?C@T@0x9bu2v zpIt}r=*~tQAe zSudaR%;?EBD5H=!tca)YO!B*ymi$>SPEp+^!s&&SdRt%ipLsZ~$lS(N#!qMkNO{AL!nH^%jC3X>H?~R< z!BRNMUP?rH4L9WU=2L=$`>{ES?+5eu$qI=Fhv=KUTwJ7yH4?t-(wM{VKL3+{8FjR` z%QuYE@fI)-)`47tmBcA*s&RrKN!tPy;l+J_4zG!!UFFLmyZAX=tXf(pWMh1NiBZ`M|59yp636C`KhpYPg8tkj8*W zzI7tW%TP_@BDP!b{9YL2ly+28v)v&J08*Zn2Dp6)u?d-b#_pcpILiZ^YVX2HE+Wy7 z9s!K8(KumcFzEhS2tK*#XTOZ0NjEOSjLOf?j|Q+7$8w|Q%=lV8zq)y!)bw0&x6j)L za%QUjOCt7>zz<9n7egL_ABcX*!QC>uAubTaq}Cngph?KaJH~l=zap(&}b>?gzyx0O!qr$dYf}%ZAeH9;KrKH z$WNc3qEqj|JS*cdOja={;l9|}X@<21aKaZMB;L94=y-Ur6+|GkiVEfM;0Mv`E!P1) z>=dv3+4~{OZ1{0D-Slb_$Ep%TQ+nI5*At~Dgy)fB>|MTWk{S8|hE zD2LUHt|47RZzeXbXYRke{I44!A!Fm?kaY<+t)y;j^wE%N|rKbS;}@tiR7?7!$O z6wgsG|1L(+eu{K7%jIAidUMkP?d@TO8=C4=x_726{L7KbUN|#LtNvM7cu(ACqt5Rfhc9z38v1+cXKW9 z`_bY$6;I$xx3u9I)z(rCE2plZgt?WUPr_W~gWB68<@2plU|eztw{%5lA9%J% z(%MwgX24=eNg`LX;stXW6a)nivBRU2Oa zU5UiDBfT;Zq(_M(L1wY~3t+ZE5s<9IM2ftKfn$Ls8v6R%U?N4}AL4mO5Ey_R75yIp zw+OnTYr`S3+EMZ_GT#~(Kh744_b>0L)K-YHL$ARB=f5{*G$irexkjo>E$Ij?Z^xSi zG9JH6^dLS;=0CfmeJuRN;UdBQ?4?*O`IOYqB|H2$ z;?sE}C+7gUIt9Lx{!6{b%?{PpC+A&-nKUN{VeX~9o0-E$ikmS!EcKqKd4Rw%Xy8bn z=$)PtOPI}kyO;(E$`!^0gwW|uVpC&lvFXfkb7E)1FLttU@h2FQTYnZyPkmfARd{L= zwE14qnsgyw#$2_sim-UTJNOkhQM1vi4)xuPCu#XZ!X&dpW-!W^TtIGehJKo+?Bkv~dtGJkBj;TS1kw*^|d_IasO>M2!+jzUR z7=bRc&)k$j^Xgr(9$;VAy0rW&$)G`J|0&RY|<~PUs z=MRVp@;7l>FMkc*$3LjF^`)61?+=F0prwBir~ivVbU9-pV@_5m;S?-_4G(4xDB~p1dC11sJ1oSZ|0-{7Ts!rP!L6u=e7PgDlH#K$De+0mBGZ^ZxSFVYh z4vSPyoh*%ier58jJtj`T#Z1IL#XjKyo_%DNy*!Q{UEoJR2mFnxUgOR-us)HS=%;@i zK}5TAk8ZRHz(90bzrWk7_~#9)-?!lnLsu|;0a2|CjGp|2BrYYjhKHwjWISm%CU8{I z;Ko$4!piqH3W9=`gN+mJsE6HH^Z_e+}*<&YGJ5)1Ul!o$_| z8LVk|SRnhewaOgD?;}7B>*$cS&Bxbg*T*U|-9kNH`|KP#id73UK zPVk@5g!lN^I9*TD+!?8GhrI6eKs8s%oM@16Ta=w+PNjVD(K_BE0ISs zyQd?-%sC=jzC`MH6XV_`d$oVxVUP|TC4ZOlTu?NtR<4}CV8I+l8P#;~^pu}C!pV+> zxH!VoBr@aTROQSE1Dr@3k>NQ8?<}dg$2pTqwfw>uv%>G*ed-sD)k_>nokAekPKi25 zOGt1jdV7v`O{)i`TM{w+tTBStW%Sq!hUxFBCHMxDF5ZcF=N4RKnxSO+#6n4yx?qd@5AUhL zsHSO;m$F{0b~mVOgt)D+c7NwKBZ%?E!n8%dV<^!b@Az(^H`dkfiqOm#Hc-qeHCj<` zMMjEdok`3g=V(VGZw*}vHM&B%iVcD>MR0pV{RkZwy2n;Sd;R*jVb)QRsFw7Tfep7h zDE%rY4u?XKJhU!uE!%6MZHl$G(F>gu#%+DP*47HDQ_6F_@s5$&%n*wqZqrUx;jQr@ z4mPL8c*U3XR&(F|`hIa%!|OD+5~*2)J=-@)4Z6;(92A{$?~~5xT1`yu^Q${-6L)$# znw>3sg-UvafXPyGSA(3CvUq_URVn{u&b^J@ot?7Cyc{UwG+4Q66_!Xs5rpjyIQX4$ z+hKouDeXI2Qq90%_ux%U7k@IKUOfS%H^ErFH$2Vub}HF$Qn$79M6vAjBHpnGDc%Ut zX}V4Su?K5k(>FBDWvQ2#8TmFYPFb)0>i0m|tsRUkeR#74F4BeUpkjDP>yjw+=FIHY zaQLQECz_Z>S|3X)?;UAM7KoB@j3Hb}^&dWv*QZPIk64wEAzo;CMtw-z-}%aRu#rF* zIzr`!P~0WENj5E_Rj7EwgF#>^Tw{kn?hA~JhhZp2!pm$hz{Y_AO-WrxRNB3D#ICsd z=!Crk00}nRWyn2s$|)}`y}YmXJsp!k!fv|v4~WAYTXW3}+8)X}zAMJW{V19~mKqMm z3r%EtwwCpnmR5GILzAk71syQW8*M~a1UGYJ!BTTbMP>Vro>POt?x{_={;gd@9Cli5 zwwWFzN}XE7vD>MFwx0s~I96XmCti|VB*(ZL!PyPp{&?FiX?n>byBq(&?fTRcul^on z*_eeH%ilnH0X8M5pqKq#gX%pXqEIg&O2U;M6y4Aznw%?ckat`AYw0eqLXVQt1~C`} z^{a##h=vIrAk*S zXk|;>?p+DW$*HNw+Av4r;0lk6`(AJ1|L_hByo#PbgK)*f%qDJ5*w4SD3Bds(VP9i< z@ZdrCS@*}`+3%t9mjpLTw<=6l-F@$o`a{-pB0%R!^1yJG{GF;xXRPeDyZ_k4=GwFB z$_fPCIL&{s^M((icfT-?WGH?JM%Fc7zPDiE_{vJu1jA39sLjY>Knwkr;F#OZ30?CH z7yGK#0(?hC#;slQdIa`}DHaZs{YR!T=z5PgwJy!lWc(un;6vReJKu-4UA&F~cWx!N zAOgB@%{o1Ytc!_blzl!!?X=N3%X`utpEx)v|L4}@*QHy25=(rZ5UcX=T~ZclDqv#_ z=mm6OP+>F<-b`LH+ZQB0fr}GpWg@GulSy9H=CW6=fa}n_5!;7|=8aK#Yl#3nL@=Rx z&j^=@W(LLbAJu#24}ML{zu*SuH)l@XX^4FLADSj&v}agji2pHol;7B8eJ7wKs@6U#j>>P#J^Oq2< z-g@4J%m(A-yZEg_LJnE_5b$R7Vmm4Z5%)B1LXdBxSM_Pq(%o(I?OBOlg@q=>&PTW5 z->-!@N&tkeT)7h7dB}iz#xV~{6{btagz0dz5bYm20f56>V&QdxFJJm^nnJYnWRUAg`Iq(Gf(rCgi~+JZjHQW z&s+ghwNUfcIweU_OWOLe?e+0PjKei$jBz+IuZqdWQXkGnvBhT((jK0###M&R_WZRh znk^ise|kO~S;{D)g^QI|!ru0nExE3#rN#EOHMApep}zg%n5Ewcg8PR!L$0Jx7g_Wq zE-qLW&kP@W-YgU2f$ViwYQ@S<88jb=_ZLo1ieN7_hrOocFfA?~Q! z5xS=|JFy2)b3Ew?aYRO=#R9$x08jFPX{B|KCopE@oGI_vy?X=mI*C17JB?|ZJo7D- z1M(On8B1Rg)+VEC+S#OD4>GAs*l}^$3=P=>r)hmK-+uqZ@<=K+=_xnC^IBet&($__ z6fq4Lf)lb28JKs^NwGci-aFy2;LvqW=|i)6aD)I=96s_lG_Y>AqzX((EkG3YO`vp6 zb&n3dJxgbf^W9|0hkY$f%a^MOqBQDzoO$%0cMToTLEeGoNH;z-AQuUq+S+w!;k)1Z z`;jlzV~zQKe6TlX)!VdGQ;&!sMQbQ2SB97Od74{-?|kwoidn4WHS}vCAuAmhA9fv5 z;aimIRx&DjX?LA5{^F~6XK`&Jg?~2O=6>?ReWkL z>n{FQK@{MbDbbKIC1z#UuHH0yX5;d$f`Tw!eq<^-=sLSARo<{5Ba|D$-)e=n6l$9eV$6XJ0*lI$F1%5%vt1d^rVeUaJ*aqzbA{Dj1e))%;0r@ z$eH$zls^aluY>FV65rJSVB&cD#8zpDT{vE6eD3*1yD0Q3M}JRMuBuuW-I6?R&IYVT z7t1Z4-L>IRX+j5+MjQBG!-O+IDjiG{(xQ%nEv!!+9a>7aBoj@jMQ}cOWe%XPwVxcc3xpe!N^@$*PuT&lPU>-PdKc?Zxt* z88M2Pc**zb{5AvGw{|g~L_XxWBqRK*2Bc-Eq((Q+M2px)O|GIQ|46|ZlI$&k#)@YO z!IMr*P+br0&$Oit5r|@VELV@e#60hn!}dSB*QjZ~y3!#ZOIGx+e*zFvR)4l_H!&OYE$GukD6f)*#QF z)Un~qDK@%x!5Hc1QPRd-xpKOshFih06D7xvZ`E2Xta6WCAU$u^PBy#ey(2guJNft3 zNUkK+aUcGR6F6sh&fZ`ka#>AHxjxh0KMoOdA^|xQ!za2^&}S!KKc1Tkjs-n^q*W+b zIB6-d! z{)$9(-*1DhU93e@jUE$!^Ehv;Sj_TQ+U z=)wB7o6VaWf;K;qWwE)7<(xgv3ASkCvb0~O3XVR`b3X$W1>*w0w6{NQ|MBB#&5@sv zi>{oBr43RnRTh_pP2S>GQW;~Sb0_|FIk4`pBBY-(cZV3sFrq{V>B<}g9p&(q`Ca#i z6C)bf+a{V*$IY9ok(M3#=(d(fK`W1gYLh*SklB;VT}ZjsrP}S2&y90x@Ql9i=W>#4 zsHk>#mx)28Ggl4bsb!#%xCo(J!}MBnL?wM0M35=b`8PHognp-l2PMG*OTkdsFUz1y zeoET(xFp-c+ zM-}@I4JD1Lo$p%%dF;KA7{1W=8O0A@#vM-Akg190pB$FbrmbeQR?Dve#6R#&o?8v^ zo{*!6P{afUU%!2;cH~G#%jBl%s*>MLIwu7YOF#~Qrycu*3@LyXj&hbbc=Yx4L4~f~ zDJeN4!oh@?k>&6^Kg=DXf$ zxp(sAPSOxUQcIpwI^!Sj02VH|opi=MCNP{@VS@>vs7YT^gVFx&*|v%A$M|IJ{&I8b z&UH%~4?<1aa8UPDEXSe-%NohIp?+&GOZaY;)3y;>S!>L7i6bs&i)hAk;(60tmZev0 z|7GW*-W&HdodcTqEf7#}#3xhXwCALD}W5CJgE<6x|M#I(&dA z&C>&7)9N03TLM^_4fabe=B9Gy!cJeE68Bd5-^nzGetPCFUMq@&XB8_W3&?aHk&>o?;D@b#Bp`@5|DY2XQ42IEm8{>SuX{K|PFx6Ge0 zWfZQ_V}}Y5O!x5ax+eyMqn_4r=t7pOOWy@0D;dDAZf$M%7lM_5pw`Y|DnjTpR3B<1 z{xt-IVfjrXsE>)qF5}(V+v#_dM8XoHr^Ug$GBTd5fuz4E!)D^rcE-PEZhuGPWkpdt zaKQZl#YlTGdK@H`1_TDClYhas#OwiF-bi0R@yQbfKw)^~@tLD_9(5*@QDtX9H0Nn< zZf@Moq-JpQS!u&)+kSWd8ayZ!2yN5cQv-eHgG`H*%;hcl!2_tIq-1CaA|lvQM**RO z5`Sx7YgN|oZ;mPA{L`8GU7pHUvOdmb-r*O+uJW~9L}T!0>b;)f_VATM)jtbl@Zo+jW+z-!miPiV6?!OQ*rJ@2xN17J}o*&!BgDGh=T zhg{|9WtmPaau8S(fE#uG*YARSa-Awm1+2~fX5N;7d)RDL~)#8GYDV zNKFS;6_1I?P)Fx+HVB559m^?U-wxcnL00j3_;iiwU~nUmGuYIlRgR__$pN=$GX?ET z^zr@KRmIpgC+BcCnA~DWwDA}OzZ_(OGl_x7tP&MHqAo0wam^CDU&v65)_Ch>y=*MC zW%lYYsb^+HKBVS6ldq45)`7G`-TISMZ$0+m)r>%`R9#ye?`u9d=7#(tsG$<;=ruRF zn#Udwu4j8q+!Q8*%-POw)8SV1c0ggmw7+ZM(|TK48g!dO1f0LFVOFp*tK5zq4$u@f zbV7CK4E2q!OEv~Pz0O3_M!N>W+8|Y(zpWwLGZG8oYirlUQPJEl4(KV5Ma}9TXVk3k z=1pFM;n$96Y8Knqb|Q#O$g|*xg{DERty6Re>?9Qn@#2ZMI+BR9j`AG1;_cTRHiX-{ z_|+>_rJXO_6ZDZv>iqkaan12_iL6+liF*U*sCBc0cS_(lm zRaJ~0S1Q9dRGr%=lk0US({D?r7Nu51{C823)%EyM#d)!)tshJ!_KF{+$vU_1;=`q- zj9&f?v1gO|N54dspBH$D8WCJ}8@_BU>%d2^pr%{5#BX(NMW6E1s+M!l4W)C#wkBQ7 z{8*nqyf1lb?yih)&#LT|37CM;LdPJC+=jE(9mkMVyY61&%=)kkC>#*0pa#hV;(xM;Y zvmu;S=zxG11qNw~w4o5_>;LDfR^btC9 z2JCt!;Q=0t5o^F|cFo!Bd2yHk;A=3-KJ)FhGC+))_m1K8DhUP5y*qcV$_Lx~?c0Kw z;vVAUmLRse^o78JSyo!*@?}@paxnOV<*ztOX6}Nix?iYO{h0()7$|^~nhSHup21Wa zhYE4Eh#O1SaE#{XPrX)7OG!zwX&M|8YlyX!7ZE|ygEg-l&P^;Hk95$4Kh5gH*kecD z%%-GUySd*&=&+Zy6$kBlw=(VxUp~9i`z9Pxi^XSvCng2e_f*O*^9kl>ob!M{R5~=& z)$vM=E}yo`rFDJ4sA!n&*pi$_u1tJx)ltQNZyD#>MI* zPFS|Ut-!y?7DPcfoLc{pavVD!d_dVszOr8PZx4wK%%2yVVq?H|867=(dXP@Tqj5Vx z)A0QJOw5VlQ*abwn=O1&8{VEqwhICc;xzcm>7+xS(pW*LU)on_I~edHU^Qz=)`d*t zw~fSx!_$JH7OcZLwnbmM>YYkCE_18>dqTcOD=+rbTF1pjMnDOQKQ5JA8=i~u8Fs=9qeC{Dg*%$I6W;$0B$tUy8YrtxP{n?@2khJ1El#Hih^RHjKwm8e{ z17j)#8yFvl1tJN8f*yPKo?~4w`z(NcCs_rB?>Ym>{s@0@48-$qyCsP(5or|!m<@FOe_N(f{h9fPODZ-}WmK1l9B zTl-fB$#2OMm8G`KdV5thnAjhZ>kxZARl+gKZ{PL3I!F!N!vk0yx~2 zxLr?XWL&aB#ijACqR$lJJFI~3 zFmA=rgVV>ozU_N-|Ne9FB4>(rEQ)chHdUDMVmcZE?w`JU&1~^-qNScjkf&bk8d%M1 z{MtM>x_G3RR@d(+0 zKT4#SB({JPP{P`C9mrF6lp@px`$i-wVPUc9OAneT9gvmcwj={Sg^*~t2ug_P=&bxF z`TC+^UiARf(NruMk8o*8%@5ggNnVYNiPA*QugG3BK%_oB#->DYS4CZ_wTHF!l`V{u zs4th?QhyrC!K@Lb&dt*qS4Stk9a}GhK@v)gLd$}hyetPEP=NL%O2L_LA(Jb6u{s{n zyQE_$XV6AcSzJ;m8$rpvA+IPMBzU8 zS}Lc!Iq&36w4fQegsr}9>+m$}%2;kXI4p<&yW2AtxcK)4a@iQF7z(^}IB*9R)m*oQ zBzicr^1r6@vEMrCo@dzR18T`Wr#Yc<0_b;jVz*HfG(U|BFj_gamEc5(rwDx#pl%>O z>}Dhnlit|;4Hi9M#I2l;M@UmyhUE)P0L4w^Yxx=lAsrSsvLUF^`RW`t;GJ0LC5MsQY;lW3Z7oq8J&v+0)Q~Lk=?` zhf#Iumh;$FCEkh!SHVMlMONtj_pMBxEH>4qx>eoeka6w1SKiUt-TmWNUxBGu3|~S* zX_1PH1=q*fm!yg$>o;RkhFB8fr=|j*g0^vb+vm^KX^92~kF7_&5H~we=Q4E4nIn`V zwZL%mRhn3`ex&xE>YG*<3l>#lhh?A1{zVtt^o#TsrjEjUaQObcdwT}c)ObDf0}L)D zo+Y&{;@uiAOL70%$9wEniFNQpeDs7Rt94*&ug^K}9-o&7M0PFZ%Wcmtj+uO4 zvS!Kr4RUD}T$;^7>CUw~=qfJ)R$lGPN)$LBpmBN$Ir>Q*ajx;+PR}7cmr;BRoTuWN z$9}`QXf{In^rx4s$<G>em_K-7ZfF`GDJ1A z46Qz}q%@Sic_YCIdY`L)G}ie)V)lu-SXD-_RkzCE@0WtosB5_R9{70pd?C0JtI1-y_XG%$h{QG1GVa zpFEeZNQRJmK@2wh8XJE^{ZQNK4+V3KVa7+KKP6dDXCGDHJB%%9f~7eA86sdp7xYVE zgMXX>nK@KnmF#KQJ>AxplamwWx(>omtG-89JxQb*3{{OI$=8UTz)vN{xy?%e%GqP@ zCCqofW%@||O>4?EkgW+Q?{MF|Hq>Y(I&!Y0-IY_4JFqUB(IsS#;<|{{zm16RKYZv= zu1hcXzlP>%Nle1;Blm1XFLM+kkIKr={jQ)8ktdEh7t|6k{2%Xs5MH5#2l=EsOwmbI zfYhN;m_;;{IpR&VLYbmb%Qm3czxk_>>L9m^BP;W+2DOE^p#H;U$~De*=VSa}O||B| z^z@z97eQ56#+Jb^3ZW^ewJHUcaqp6&w`;3Qe~soor9=uGI)#R+Dzi*C_Nut3z9m9sB`mz4eQpO&e9)f1JVTXgvP;;aCrYG zh9aaYtwV!Pbtdka9e(@vE(L`rb~9?91TnWxxRs$y5h!8y2bCJf6TMsm^7bWv9vKHA z4tIoPBl-ese#OaeYA?pAYDW(z$r>F#JP+(XNkT;PJ(QY^eJfkBh4r9)LmyvFGEccH2HMjmYs|j6-+bv-5l$YNu`31LIj0X+#U@$ z*qRNFmC3IbeuTk9D-_)dv@ZG_kin3@|9BTkr&}CUhlIojn$3g>Xf`!BhS19GYb=SH ze=&&S$>njy@K=byR=H_niTHj>=H_R&M#JNdcQ^%@M2=kdjy)WG^s!{bvahiX$*V%p z$@VGlB}2IKVxsQSedl=$aKDsJXt!KN7kss8LUz5}({q!X$qh;7g$@ni8u&StRZ$w< zvAlB6#-Q7`e|Cf!=}H+f5ok$sgJaH5-E1DOKuH&cevpuaD6eZd_(MqDaNJ8Wc#Nuc zT4a#Tt#AZcIq(ap+&^k98dk1AhjM~sV;H-h@RAOtA^^30S^$hW=9RDt`UdlA+f3XQ zAr@W2*xf673C8cHwtghusajd*?iHm~-> z7qcz7VP8p_NVTXR+m*Z5wcr@J#XeunVla5`{o3w4>`x0{75!Bz`AWTFS$7nD-aZBp zT6oZ8kidx9C%VhXH2&#PVwcoBAQ_r{=+kMz}4C(8cztY z_BY-ULAOuZJZ#F1Ax~cNYlLb9E-n+cImFrJOWU9B#z#ZvPwWe0Oxmf#2OV(^B%RzN z(h(Y)t^Wjwv8oL^R#HA>88mnKNFb<-QeSFbHBwjK#HZbME8cH;WqCYFLif5xkM$C$ zzyl_g393^$Z4`M(2s)^`w!ylNoY zbu5Hv=9eC>k|d5jU0BQc^~)+3lkZZ&s>unSKNGrY&b9U$l~;V-m%OrN+&xEU+*8T$ z)T(?%&+WBYXZ=>kxv%D$Q_i3};i_etHJ}apru7ODB>-5PZ8L|`99=)^;cHX`-v|-ixuA5S&>QWoz0!dAEOT95*BJJA1kZkZ#yPrw`;4 zk7zJa;8GvXn|pR!p@lv>I)1?Csc*mVY0ao3X8ohd_?5Kq|5JQ{@8LI)B;u{Nwz3Ma zm8!TaQ#GH3rOzLZ7VlfX3$Y$|+=LV+{;)_?husH%f7W`Dn3FaXAwa?|qCuY6jA1s_ zGLrAI)YuUVJeB9_%s;{J{IkL(|2(ZNc+VOsVPe?eJ#=;A)L$rx0ulizSzwln-17H`%eQP!ogm00pd$!tt-bM-4=^3A~wzZ~-fT zZYee=$K$$Ej)eB?xmdi~*0C3#YsGy-zfPEgPFamjTNI{Dc$`yAvKQf zyZ@RIY{@-qi-^ee-598ABjG3{X=_3Gkl*=a6kBpe#Hqu5LDl>cYpvN_?Q9w#y%l zd4D{IRxmv^E)*0VJ{IdU7W-DIqt&B#x6_%<>Nx6K*yDV#92jlRv17+To0YV^E-Ta3 z)y>AzzOr^Yy{y{N!Qq2#ra1ZjbuewnAJCDqQ+}j7Jz`gwVS_Pqcov-d+Tt)joW>ex zSy{J%==CAkiHV`QqCzVc?==DrfVI>qZW-(BtVyUXMXg9S1vaXxrd9wS-7tj2maMF4 z?+L}=svBTNZO{s7Sh9-w0mviYr461@VPJ1K{hxMWWuG@>Mq=3Ilswg$tU1Cn-OnT4 zeS}h6to$&kOP3wu5kNW$@m@CN6^BjC27$5@OIVU^94$W-*$(0z&-9o1fc?fuTxdcg z6XLy-Un3I^zZF%*(Dx=9@C=12pSu3R5P|!ztq$04k0^UTMCxS?aT>A}!x`jDb7Tr3 z8|iM28q5ppeO%2QxJ6VHoQVyeO^g1t_w?jQT@E2}K)%5#S$bJ^u7C0yGP@KD^?8Gdl)qK&}Jp2}K z{=HF}@PfARxitPdj~Nf_R2^Um!w&2;m6DSiSDXH!#&{B?3+YO>DQ+23M4T1(jhb!9 zDnM0FYvt5Yusy?SIW-T7aEpORckY(JQ-HrUGlXFft(JoFm!IxuflN! z$rvUpn(?NF+|i8t_y3ArgA$=!MFdZ?*+JU%WpIP+85JY8B}={v3mo)-FE+)EafTq; z4KfH{+BA>i#HVGXsbu7|ZP~mTsv_E|C_H0&1w(o$(hiefT#_f!G3wqsp)2>OlzvCk zY!DCW$__lq0Of)#dZY?+9lCN4aMUd2d4U)&S|*N%;CTSVCjGtF(zX<;u|=t) zh$B^$wiq?S>G1YjYF+G8xBcxeaeCM8?>a)+v(b3>G)F`st2Mbb)wi`bu;mpLz(p!sFo;PBJ04^pCl( z#^+zM^r{tLT$;8Bn1z zG2zy|E{LPUt)n8|=b_LAu!so{Hor<+K;YcQ2OB2v!WFNJzlpl6Tj$Y%(~meW*4rpZ z{YCtO_`bzhIdNv;ILgY%;JUW81wZFC%tHBvSA4@tf*R;s$zAk&upEuOC@szKh#L?7 zb}TRjtN`|ZC&v-vC-G}$y=B7SxE_WpaiQ9CTXRNv`SbU|t9qaRIvJjykG3rJWYbS1 zw!=NR)6xp!wkdlsb8}j5r_P%_>(v*TSJUhLPUVTuC0?MfQhm(+DgqUO(-~vOp+hjp zPJ<^+x0->b)mo1*2FoC?w~*0g!0st-R(0q6gCBQ4PbcpEG57cP(MoYclA9r{abTjL z)iXOxf>1JL^dz?ugZ15Y!A6j>7DGM#+Y@+sIy*ZNq#Rg+5NKk78g72rsGvw9#zP%W zq2vh2jR7u8%3%W3+{`p8IFs^RdLu3Gqt*UU*`{N_iLo)>kRe4%*v+Bj>FLQdFl?~F z0uneFiu?~Nd7VqWm}wj}MZm&azww{MJ&VAS?5<+*%|K`7R@twE((IParNI5FS&*&O zG}LW`?n5S2b>Cl_7xPK$OG0D?G>p*39v8k%?OdnCw4W}$!YuI8&GGNfvwu3-nTk% zB+jDd*)dW{Vr5v)YyH(SwCyMl~mmnuCMk0k}PeZzX& z^78bGH4qQ*g;pOKBI)TRY_~e8uYXs$Yn>9~2vS+nAa7^Ou=W(v8LtwjPq|hp=!uGo zTN5HaULIE_NJLsKVLl*rzdO`4w9qKG-Srw`RvGf#H{872rMvcdyeQ}0ttoU+hYZ?}7BDood?!~}rs z(hZPxG=X(X!*;E_;Z&InN(R)(uQf~RnG)LnFGocuLY4sEEathqz{e|f!9KwWoCIe!v-MbBnY@XbM=CPCm za6wP4XoO_EJK;K<_l@Z~^wX{zW+cVgswO8TkRV+Wv&tUn7II3b_O)t3^>T$;G? zk|@HaLaf%l!(EA(2d|u5hb^!h#N1V%vTC-1F6M#riR&;+kwil4D$sp&#q<%n&;m;n zcX!<;v)-Nz9a2WbR4UfZ1^DgqDdJyn)4Wu)TRpTRs^eQ9>t zq*R*Uh2)r+;#}3u?dQ%QhOfMX0HN+++V4RYF_Tfo8CSQ+H-E=;uBsL4IY2XP96}DA zoq_AE2tr~iT5|-goS9R^F&soHp9NkHyHuddLOqSsdPbw%Lfb?7yT^J$O)4X3&0eyC zxcIR-RdE3^c6q#80*=+pigdQ-#;%L^>~1ehLzh;eY~#noS9S4qGaw#^KCI5K{1iWw znH}2Xly}P{(OSMsBumxk)SQ%HrqK|%ul;-?k%1sQDS=QijIbIek08_(Guul-md?;V zura-VB~V0;0kVaa7g>5?2Zu2E5r}EZ>kBRV(J&%3WZLnBbw(xA9E%vFwlA$@b1N2H z3@r#wp_Kbt^#aoS?< zScdr^z!UU_qa9*Cs?b9&mEN-^#f=%X`ZBw$pud>2xUJQ_E6Hc-i%&)YJ1gcZnGYRE%TVVFM#JO>Cy*x9N)pWyPKCziR=vx1Y_N*rPz{!7=*81q*YNs2) zYl)Z2a3J8#N*UwrmTMh7!U;P2;FYNr2tYP=BrF+)2bbcHDIw_895EY# zYsMW2w@!Vu391h>g`dR7nP;w)?CGprHgL+^9oX+U1A zM#-0H>t6D`_-9MKdUdPUSwGX~FbzDgh?n&6mQEY%X2U2+Lp`Njs<=MorLII-dib)3 z?=Ns}VqJLkXVS5U{ld3?UamRHEl^#r{v)1L9xq;;d{9EcG^>++{8_QNzJ6q{(J(FX zwTxrHm#rT+7PN-H3g3zHeHUq&kft79JKh|=K~kX zNCIoO*o5dwyKh3CocgnCI0RYRRm!mnnB|%HM8gD9GDB|MARYi5nhIWHD$X=_U^>(% zcKiL;-8V)D0Ze_vcH=@!!bx5TINRaSuF}=^%)Sk0$QyTCTU)dhhYr2DuxM39lwz_7 zLjN>BK7RZdI!k2D8Cie*S$Ch^5lXOz5NO%SP11c-zaweq&du&R&3Za*R&r1+`;e9K zx5`JGbK8JC5>M*89b^SaJPV6q3b?LbZZGiJ;XCGE2riC;gz;9esVZjJ$j< zM)4<4Dy}+W62DqIR-ff~vdXwV-z!G~V|q5=2=oFggb;X;?%HqqN<&u{=?ZbT3NJ2R zdj>YO{dP?mhtE>ma2~M7ni+dh5OF2tC!+;)L1uSr5dA79*wwL8eitud5Gif_b9wn7 z8TiOL8`=k)EHi5{(IgBdXeV14#3vbax6!>W#r;foTki;$2j*XcIYce zM{V+<7vqjKeV3{3oP-;|y171=Uk*l(t*(Flb+)7p?ZC|uZ0mUY@&n#6{M_-z+}S82 zGV?QvFrm8iG)gKe8g&Km&S}c66JTN7t=ORZX}-fOT|ZjpsBCfc`OHy#beAq(Ofo8H z;kH31d+t?nF(M2!TgYU?Td=Hdy}~1LDoAodSr?A({We#wUgf}^7;3T~y~#{t3GhCr zC1hTsz{{6YEYimOmKYBPgt2H^N3#Us_`Jx8H;MgGQNPW0hY@tG3(}jHoEI-xq#yQi z&ZX5NVo&emF}nvJCjeJC2HJ6PPIKC49~ENRS{S{j9Lu}kwDo_$n&aO5aZ#Dd#NSWj zo;a3!EH`G0go9aEn}aE&;O8qbh3N@LhiDn13bC-T zbFxtZ9xL!~*4HL?XSA!dG1meUAN5UE7S`>8?;apSeAT_3kzCK|akjqtEC;aa`1b14 z8*0btd9rPS#7NDi+_IJ zFKeObS^te|ebr~}nYDRdDMO8jt0sB?d=Ur~@r6>2FXXgtU;A7jXvMELoJHmw?b4E8 zHzM=u_5>IYMhg<3U&ZXJ>@LE?BZI1?`@7D_AOjE}6=) zU6|0JLQ0RMP&lzSdDW_&CPf!uWUwv=n!&zHNXV-1<8x4Z$~jJ_UU6}9T4((H?DPS{ zq{$0E!4N?FUCRSX(wZi40zwh|#$8$2FV&`XXI5>WhdZ8EC;fT58ho`?gtOcwQ_g%- zFBxP^#Vq%NZT@jWA5;?fJn-=!C$vK=PUv!f^Hg-?=nwJEYTbpuU;Sxq2|O*7`7aO@ zW}Xe?1cFbjM8A5p-&H;mXMu1ewBMzrD%82ihP$Uk&|P@Ztlh4S!)@@NQ0-a`gN^ds zVgJi(9D!gapk&9F#DhUUC>a;TJt9)t?tE<`wkjAOQlYMYLml^%cpVxN@_^6EOh>04 zr{csTJP*V(aou7NQcxa~7oE(N7~Y^*lFCD=qBXUm!``;7!TeK@!f#27vSyvE8QT;^S&I;3 zN%mzzma$JIOEHn%SjLjEuVWj_IM=B6y#3DkoYP-=jG1Td=f1Dydws9(^-pX3miye= z$PfHcfh0(43$n8t7Ii4m&|GLr0~~hFk2s_HL9#9;@WqSsAhFhw>Jw;u-OZjwk|~oD zACR{RqTA(vH*#K~Mjt9Mu)D3S71d6utc#S8<+%x88*)>CjZf@rbu9!ktHs%Si|@y) z4WNY|zIeXk;qGz|H{(VmtN`E%sBKW5on?G#owjJ{wAXtL<75Tjz0eY8w|hkLB&OyS zU%L$0QpVBX`{d@eap*7k3$d#`phBBK_>>+mbT1M4pen4EQn;Bx6S;P+_Dd2;s|HM< zkZLM|U|P$eC}1ch>J{MU7oxnvQyZNN)DDDA!@JAKoC~opb>&^A?^|1sC#zw94b!g} zQ8$i`7cn&XfbG$Py{+x;K$L6AVoN82Oe<;N&hBNc!?=0i>)^Ps5!<0b>5vK}R?{vr zz$cpZd|3#MGFeDygx=djA=-hO-Iz zK|SrXjnNs)(8~DPT8jT}9cr_b`@PB$E^mUv^tEYo*2W9eD_rnx-X(zmF^C5P-K6{z z|G>A`Rd_VzSm9Q2FR;Ff1NJlhmE7tuu`S#>37kyBfdd>Iu7{TZbHkf_1Ch1Ef1Hf1 z3Pe%i{a>GM*?#>9)5BxVlU2A2Qe^MZSZm zzc08EjQ(+dc;D~X%T(6o#Khw^IfQ7xBNpzAV$|O*K<^y%XKZ$?3I%W6vgg&VKY#h5 zq7)@3C|H&8f4Ln=crZ}vg{I|W*Rs*+Tz@+gxrc9IZdA>VzkSZ%9%Ul|gd6|kZlK@d zf4W2n3EXR@V~_s*9T+}8N~7@Uzkcs1eCoSLp}W94GCVxvkToQ7-oA6k5$wuJIM{sv z^X@MIf;dlVi-5WDN9&KjZ9nYaTmStQpe{N(H4{vCLKxof$&sRNYlg4s^O1ND$!p`| zm2ilq@7LXRob2kbG89z`<|zY`;L-YD#W|83d@*5tjY%ptq`W?7rqjy5KfKEpu}?Tg zFrNmJ5eG@`*Ui~Oh|q!zL)xBP*#_UVU8c)uLk`m3eir^1>@r9P0~l;w-VHs%PzWi=vVcy*ikOR0Ez_Zd&`6y^sb~=N}dC8!89{e{k zIAZ}#B5g=ELDlmZdTGH@mFRjaZS07VvGK&UytuFdi;Gn#JPpF|JhMW44b`ubu&$pch~Zy?#mAy_p_=x-KSsTvXWX6!PoFNX!S%22=^JPSB) z7Xq%TY7MP@V8uteGK0g%#!kWpPKQ1=98T@Vji_@LA?htsbm$8jKF4?hHl2u- zb4+8Ijs`&~6I_IDic{>YXKm>ozfp}XtPc%>bn)9Cm2zOV@1*Ow8yC9M?4jb_($g?0 z%{K8tSXv)<#<2WeMB1*YhInt0CLVKB(2-=K*wpIiHY3*J-veK6>kRm9lN5F0rs)Xt?O<5TovKM`oJ$S z-u5cZip(ygwfpe78q!juis&glw`jWf4!A{I%tk8l@T+ci$ZcaP#6Yb|k&6=bGK|0j znii}EvL94rdO-Rh_o(+0s0*VEIdR>TB%j8vM_jXt9$ZHYm=L07KcYuwTn?BU8TEsD z(~*G)9O*GkWx%9NuoFzFu)VZuAX>z{>>yw;)vpos50#6T;*tK^yT1#ryyEfJmIg}E z%n5ngOIdXc=T`he&(`02|C}!fi5!i1RKdCb;6c1ali=6@0Ew`oCULXcC6|GH^?jN{ zmM!ihF(%2o%P0iY-qFZ58@4>4CA*Nh}gSPJ+fhT@Ti-ehENb!I85G!G> z4zif}Vu0!Gk=R&7+lexk-)H?ZQr2_1Vk*uW*bSDU4VQxaV&E+rU13tW<5X&yK1?9GQhZQpgLF{N~=@O)mCvCyB_aDJ!$= z+7;S4f{KtaaSfD$(jxy#k7&*sYyzI-PKGY+$%G7>B^`$7zKZWwK$fPCpc{q01p zf0)k=|MYVJd1HGb_@^5&2^*f*oV!~L;ijTs&c5NL8~7A_-l9yaq=h0gjB_M{Mod`)_J;eOCec5g*(NhBg=hrg<6Y0%t+a z9hv|jUEV9nDzVx3>sC02pD6JcvZV)jU5DeVc`>E11CzDs%y%~O$ZYgOIVj=>lmG|N z%{qWr-V^+`p4dR0j(N;H#Sk1k=*%yJx-g8Dke~jv#h};`w6hS11sG~|+z#Fxyy13+ z+Z^Y-jYKkheC^2oW&M!<99$j~cIdH5L@#!~mUfxlGN3scX*7pTki$V*2jGA;kL_uG zC=5fi#Ixi%(DY)F0&QPsjj{npb>dsPq3w7=MSPeph**nWjIYi~Ko+ z({Z?@V&(|>K`Om!Rlj_g!@0tuJ>sa-g1>0_ts2Pd3e$HMubpne-usyA5bJLL^{tpO zkUyX_ZW~DSj)7#z%WE+sosJsQ^_x>H^IocJ2+Lxa>1O-oAFS3I(C~(8He}18>8^M(U^l-_jrm#p-9@=LYzE zp01zxBw`z5)7A6GcabxWVHZh-kNV!^)o5 z50;IQJ7z1%=XqqVYTjJCQ-6@dilv9rmPIZ!ujjM85Fx4(UOI?X3k$nD)`~09#RA$% z&a(*hP=WCv@VBYm*}p#^2mSEW@aJwwQGg6ck^=2+T`s6~tW7w9#619(LaLB=&EuhX zgA{uc7G?o?b$ z)O!(g;Rp8;NO0@lidB?vIH_~8PlzX=8N7i(BgH*9oOA`k?jnFKHPC3#6{-TrZ63sK zq@fuKV!M=^aa9xpQz~G1f|Xr_#H0)L^Kg`MZHonSXkY|JVdNO-KNiW>K@8@!57qH; z5VChO6@alpUSM?dHaW-Pe-OdMfk*8!0-LbDlst5i3xG;EVdy3tZHTXUv7bM04U$Mm z{0hpY9Vm5RjfqH*XK8iTbpxPwGd37f91#1Sm%atU*{Qb^veAp}rFy!x5d<~ABKNe^ z^EuX$4hWl`)gH)(qrOw_?jKg?k#4q5}xeBzwrP;t+f( zDJpIo2Fa!z^pU;Tl;aR3A7JE_;jmcx3Z|`DhFK@(A(BY&g?TT|;7Kw+0pH7QBq<_K z{QjpW*LcuhWva^{9Bwe~cC6@xf)&8GHXa_bh$&Vq%(;W;wH$lEf%XyY5KpjPZi?Fr zqA5~gM0gc(U`9t_thqdb3=|kBu16xubV^-v0>~fHEe8YgK2z+@TgQexAu3QiOL@qw zi@*-gHHGI6i&8}a74!LVIp^`W1#K-Yx{X;A-pqR%2lVwJuGh*!dH{txw}iy}(V4vE z+Y$Eij~`jFk6#i|?b&JX`>*E4w0{d7B4gI^lV~nVn&!b9D(m_0`wMF&96LVZ*G!vc z22#i{Iw$!fx;vvd6motiH;`DXa?kh#kn_paosw%#+8NSk5vVm8U!pPf{1L=WWw8B` zHsw&{-(67jsPBr2i3uPSIcOVBcm(@BWUB!_oLGsK^U%BL@VuF7P=b9h_tb`7^P0cC zRK?n}r*xaRp3pSg&E1&UZ?9Xc1G(@g39)XI2N$F=x;z$TH<=jp1#_?XPibT5RKom} z{4KKQ02c-;(VaWAnD|qw;ZQ@l@7|c5;;Z4p0?(iGN_*xTSMxf_Stc`P04;Z>X%21M zrPcMwnmC<|c0+VTI1H8V7{O8ZB6!)^^X&$dR>Fa>m=S2(x{q7>O-M*1V4ebHd34A{ zAl_}MHZKpFv^1vfd_t6>zUNX<)Wqb_v^pi85}{JsrF$it21l)N5!!*^41dtezY&bG zfmn>f&CP9-=Lua66zxUOfO0!Ny95gTfSK#@+o(sM?{hk;yqVwTo+%aP>Md|PI7D2% zR;|N3II~D5&zG-E;tOi#LqlSVHeoU2IlOxhogMD|d^6)PJtgfJZ2>P!-<=QYdcHb2 zMrBhAKsx$l@VHyj{@2MSA)mfxi5XZ$L`w2HILO9j5ywR zK~n6QYyH*^Pp2ZHs9xR;Z4hK)Ycyq!)Cz2runWD*g}`)&G#uzS@T;G~>P5seV+pNi z1(EeLm%W+6V8L8<=qeHy|CpM3T|^|9u+So8#SjCYIW8bj2GerPAk3Zwn$JgFm?5SO znL+GqotIM_5u>K~y>`8Ix2A!K+>OCOzVE{ZC3Nco#}QVLXx0;2o$TgSex!{fJv_~+ z*>}A)sd6WfUiJ$mnizK%ubXEjvN-^S)rYzw`5=}zE)*iX)5|w z%Uyrf|5)&gr4{ASWNUJHwU7tkD9hfws|jsYFWF9XZc}Z^61iME1=?F;2r@rqh=3C4 z4&uguaIw_b&TNY&t?Gwrbvv!O0Rc1?&+E0Ji(-l_u!aQBvRMWLw5rC*JY5&1Hvz_B ztg9%uXb5_GdS_fn=<2zC1uB zULn~a)$X!4ag7!1YhWyYZIaJ!^gRn6Sp8F~u=;)ZU8;x&RARW=Wf5BuzIJz^92(?0 zK*brIu_@3;h$3A_l3+YKpsc!XFtPxEJa8&yPzZviG2ML2u-G$@mP(MrBO+AR0C*Ka zmbyS=$pizXgaA0|GhW&V9D@kgP9LHaftWG7NWyET{P<{&tLX)4je>Lq9uLP)(HHy! z9+G&SNlQeOr>5TQSGEe%VoHkVwpZ$_!`E&;OxTU|vf2##|NP>6s$Jf8^_leZ`QW)dEig>7249BXtTXQbBJM>I8SLb)%0>ty6~#Fy`Cm1zkl6I)mw#Q10< z&`!on{+!r-w^%ouhZ3$XjOxsOFF7jlg($EUixNPHiU#}neOzt^L2KZ#-gShnl)E4e zL!$0`p@F8XrpTt9x;^T7snkpL!zySdLlwXy1qB20hhheSMge zWPg^k=-CX)EUtZ(`}V;WJW8HzC}vO1$+_D$*vw2B%+FEP1K6}OVo*8ZJEDjP0cJ@G}fw7op3QI&(7LAiYZy@wt&W1a%5kY zySQ&!6-I@Of+?>H(s*vslpIN(z%JukwIomA_S+!89Ohm3zzy4l$CQ0?Y~+t-PQUvn#p4=DM>*cDu~?BnC-X+T33{nM$e%n=*L>^yL0l)s z*>SvTM}zTG;))Op9ahZnwQ+TbcF(rR{Tmj@N@>9Yy7AE{+uG?H5w#9mx1{r^lBs=ZtR8@V}*;==-j; ztK6DQ&)TzY-ur&-N?t;L9&->@kw;WMQe0rZXvmZAA|8`ua6l(`z?awatC%VC?n8-J z3w#cpkA2A*Hu;c0UEAqt>%MqteZ>q#{Qx4?*PR;_eSc=Sgt=u7T8iWzitx`L7G(KGS<8omud?@m zs+3M`K05!$U0S-Q7$N`Lc#4|Ju5T27KTq?`fUC83h1093F7v8e)gP=J+IQE6B3J+Y zyW4QQ??hO+v6Rvio?X^*kFsJ>h)B04sn-C87ubM?^pscqj<)cBtji@G004ucJXmhp z*b``;1<6VhP(EXs)nlXwB`+4xY>>}8PDXJ^7CBj6kGBOjbQGHPzIv3EP0wvb9+!7R ziOQPeWlz&zoAf9t)`j2AK=}Bs$FTPf|Ez}^wEgN5XTmYFgsu3#7NvEOc>MC=@vOC1 zuc3+vRsB}0r6*BHd!rWh1v3rHQPoiKkRyX>$L{3Oe|$B0D$jMBvW#F?HB8ZRys>{j z29@?EXl%Y=-=nW-n(K<`t+>uD0s@2S(JeaxYHtP$yMeTMI?upl*7e;X?+u7N{4l` zNoXrK8$dD^Ys;In#2vZ=9-&!2Q$Usqn%RN5%4i!|nHyo_=AM|T@P>@GS@Wi;nHi7_ zEwdnD$?6N@Laa`N=OxS2utLYSgxa&SuYqb_c)nE6nh?(vq}8X2RSb;;$8+7&*s7SiX!4)~GhEKZk_6oH;mAa8TxrsrMB zL@!363)lo%G79%THCS-_nxd8Ljl9Yk-?dNtHa}v^ntvg(s$5|doIa`yhAWQ{R+AQlDrFX1 z^{rsQ7rU+;Vk%4NVHZ4|Ro6y_@6`@8PIdvx1OYKTVFIPQI4u%s=+JoQ%#`SqF(p6B-E@3r~`_Jcl)JwoAJHr>mgM_aHQXIvzN zg*C|l?bS4{!EwReEBd%UU(N;FrkuWt-JVJ)Iwo3vUafx~9&LtlW=dk)b*}DPjBtCq z{L;0w^yZ12YR^NBzaQiD#$&v!?#MET`CLA*a#KlQi*)K5PSka?rGJ~_d4tTYchYd0 zAvQJecCE#^zo0NMcp_V$>-w3VsHT=$QS1`h?)U+3A<_qYkKc5_=Uc zl&x80IGH}r5T(j4JP)-aiQX`0YrHtip+R zh+tU+=34Xnd*Qjdvu3hKR~4=D=MEHw2_kF9JJ4~Su0QHu`C;jX*==`eMGg*7u%8<8 zO?1dLGxeDJd9A&4s__8PDC?=m&SYm?WJkrJ9Mbq$(!lAOUjeEp`Cp9(ik-%^m~j=V=blUGI@4uRRJ05-r?Rq} z0O3(`E?PFYSvriU@;Qmp%B8qUWdG^s@uXzxj1D!tsG4UbK58=j&35?{MDmL2=F+0d z$0AzOYzAInR5QYb-LQ{cd%h@25~93Xt!$1&kk@RxXAN4ollC%tf{;VHAEJj}=Lb{>6 zTpduo@V#Jg>XpphF8?F(7%?7e%DRxj4ZQ_{N&KMT2_Dj6IS2d?S)s|Fl#B% ze{Sx~n`I6W%<4ej^fW;ez2hFs&A3=fp+`@t4Ef+^sURXllvg)bq^1^<=m{Wjfub8# zR9G*SNbcE%sQ>)PuT4AaP2O3QZr>t+T@Gr&(zIj0HD4Vlv6D2GKJ3| z2?mN0(XJTDFOs5e;Ih=``R$u=6p12qI;hs0SRw%Xtm9)+A|!;Ac`vic%(izCp!eKu zcdZ1T-Ze}dU^#q5igA!1B;@h7`D$*pxDOHu->(F$Lpc;&$KGejmuc9_7ag1bm ztOVVAJp7t)!GhicdV)HDKM z?;pg?TZoao4$QdJ32|dxI|}XmgYq4Q)F=C^Jeqw>s6Y_PH6tnw>wg_aQ`ZTgz9Zr0 z?3~I?jqg|u5m1q*FXi@DGX&=z0Q|bwm#E2_jefKRco^Xzi>zwt^Ij6wqrolI9O>Rw z8E8fAX8`ZlBU$`wBjVP^hd`q%7mPP*oHytBeJT`b^-En$rw5iHSO4E&+oKuL(hF_x zTBa#j)c0&djBEm@pO*TM&D>TMK6|<<@vW5juzyG^6hjZm`K6iB`^@u%rT*sS<@L6m zY>us&txG;_Qav@L`QPM>m6+Mfni^WhTb;LPuzDHF(U&Biy}O%Z)h5EZ;83sj(5F%I z!54pNEj0Q9;J4mm*5yzH6}Hhji%MBu8c4uNT$W$z^(JD;sJrvEZd2^2j#)a?1N!?x zFwUlVeqj(~d5Kw=HG+QUp6kjxkb}_HIQx8;q|7$`eUQ7pen5U~cu%6VDf#h~oOF25 z3&0b5rLhO0cX?(ktA4gkVI9m&;;f<~!G>s2Y*gtN9o^L`(Qftgy#U`@^+&;eIE^Oe zL*fgQ83K>Lg(TV59K7MRD*}eFwcLh=$%8Ii3$9?N8k777hUhr-Tt#CoGj)0~vBaPG)* zF#9^Vvdw=j=iVQ{^!~VTCbmC5QaKS z4mQ)f0Ta=u@fasxeg)e-H@7e;d7S5shEPf^|LgW@06r`@s zv)*a@8<;M4l&&QuVWke<=m*j^t=GFumX7Xy$^$zzPJA$*$%6*G{tYrH(VSZD zuj&EpW%!aL#oAa|-_xhnqotkr-g@Mqe;3khlZfOx7*EUKm_nc4mQTbEM&H=#(FSfV z)w7jK_ON#Fy9QMR@9{af1^CauZ~v!Zg7H@y7lG0c{NEe>zJ-5Z1LYDBs4-@I$nPKF zofxlx-*3D><09kp;T8X0+R(GPbR9@}HQ7=q5o=~FW~$7HTjkpEj!het!G~L+xLoWs zhA>vrl4y5Vs_KGHZNloTN(lD`gY-$&15kO&CM;gGb8u&@j0K2RK5`cL-0Fx|n z8sp&Rj)|6mq0)Y+#sPo-S`7`sQw4D2c-!t9vzznIVxj#fBQvv^3^gJRHsA>>-U5LM z7VTtHb0*I1)0NO0VCc!w*Ih^*u999~ue6nfFwZOxnQPL8ksE+SpT#4fV@Z0{sHX;nj ziuYp-C_FBBsWz7e5s?S zrlrKffJdQUpPQsimB*f34^ z^2(sA%!I*aVtSN7%*cri4P`re37W7tRbkJ-KM`2#@}(=MG2OhR<8utQvk2qz6MbtW zYg}wBQeU4dV2%JNcv~Y4?6oM_n-d~m}9({=|eW{aSz%Y*wcK99*`^n zLpAj4#&7VA6zJJSjjKsoGC_y%tM9+BR#8xmHH>l&6d+g+Gp@ZWvVn?_`1Hz2Z$1eL z2jC4RN;0lrqIS50@;XSzCDfUCfS{yT+~r-2Uoc5oQK5)wmDyI6@!(T{4&%}jO78!+ h?9E{8!B?wT-|nHeOsrEdRc2gJR#d Client: Login +Client -> Auth: POST /auth/login +Auth -> DB: Validate user credentials +DB --> Auth: User row +Auth --> Client: JWT access token + +User -> Client: Open task list +Client -> Score: GET /tasks\nAuthorization: Bearer JWT +Score -> Auth: Verify JWT +Auth --> Score: Authenticated user context +Score -> DB: Query active tasks not yet requested +DB --> Score: Task rows +Score --> Client: Available tasks + +User -> Client: Start task +Client -> Score: POST /task/request-action/:task_id +Score -> Auth: Verify JWT +Auth --> Score: Authenticated user context +Score -> DB: Validate task + upsert user_tasks +DB --> Score: user_tasks row (inprogress) +Score --> Client: user_tasks payload +Score --> WS: Emit user_task:event + +User -> Client: Submit completion +Client -> Score: POST /task/submit-action/:user_task_id +Score -> Auth: Verify JWT +Auth --> Score: Authenticated user context +Score -> DB: Validate ownership + status +Score -> DB: Atomic transaction\nupdate user_tasks + user.score +DB --> Score: Updated task + user +Score -> Redis: Refresh cached top 10 leaderboard +Redis --> Score: Previous vs current top 10 + +alt Top 10 changed + Score --> WS: Broadcast leaderboard:event +end + +Score --> WS: Emit user:event +Score --> WS: Emit user_task:event +Score --> Client: Updated user task + user score + +@enduml diff --git a/src/problem6/diagram/high-level-architecture.png b/src/problem6/diagram/high-level-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..5133230cd033b7e455f729e2fbbce9143f17d8a0 GIT binary patch literal 36980 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 literal 0 HcmV?d00001 diff --git a/src/problem6/diagram/high-level-architecture.puml b/src/problem6/diagram/high-level-architecture.puml new file mode 100644 index 0000000000..4026a9170c --- /dev/null +++ b/src/problem6/diagram/high-level-architecture.puml @@ -0,0 +1,34 @@ +@startuml +title High-Level Architecture + +skinparam componentStyle rectangle + +actor User +rectangle "Client" { + component "Web App" as WebApp +} + +rectangle "Application Server" { + component "Auth Service" as Auth + component "Score Service" as Score +} + +database "MySQL" as DB +collections "Redis" as Redis +queue "WebSocket" as WS + +User --> WebApp +WebApp --> Auth : Login / JWT +WebApp --> Score : REST API +WebApp <--> WS : Live events + +Auth --> DB : Users +Auth --> Redis : Rate limit/session data + +Score --> DB : Users / Tasks / UserTasks +Score --> Redis : Top 10 cache / rate limit +Score --> WS : Broadcast updates + +Auth --> Score : Verified user context + +@enduml