code:
# 🏆 + 👫 Ranking de las parejas y jugadores de una categoría de inscripción

```
[GET] /v1/categories/registrations/@id/ranking
```

Parámetros de la ruta:

| Parámetros | Tipo | Requerido | Descripción |
| ---------- | ---- | --------- | ----------- |
| `id` | `string` | `true` | Identificador de la categoría de inscripción. |

Ejemplo:

```bash
curl -X GET \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'X-API-KEY: API_KEY' \
  http://localhost:8080/v1/categories/registrations/@id/ranking
```

Respuesta de la petición:

```json
{
  "data": [
    {
      "pair": {
        "id": "2b54a574-3153-47a4-b2ae-d8353b68b2d6",
        "is_eliminated": 0,
        "is_active": 1,
        "created_at": "2025-03-22 03:13:32",
        "updated_at": "2025-03-22 03:13:32",
        "registration_category": {
          "id": "5d840f53-ffb0-427c-8554-bfff885d6b6a",
          "name": "open",
          "description": "Libre"
        },
        "players": [
          {
            "player": {
              "id": "e8522e06-23ba-43d7-bab7-c5c1c7528c13",
              "fullname": "Laura Vargas",
              "city": "Dolores Hidalgo",
              "weight": "60.50",
              "height": "1.67",
              "age": 25,
              "experience": 6,
              "is_active": 1,
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            },
            "relationship": {
              "id": "2d29f683-17f0-4edc-8694-39e9f178f40f",
              "player_id": "e8522e06-23ba-43d7-bab7-c5c1c7528c13",
              "pair_id": "2b54a574-3153-47a4-b2ae-d8353b68b2d6",
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            }
          },
          {
            "player": {
              "id": "ddd7453f-cf22-4de3-af38-ba799bceda59",
              "fullname": "Pedro Gómez",
              "city": "Silao",
              "weight": "72.50",
              "height": "1.76",
              "age": 30,
              "experience": 11,
              "is_active": 1,
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            },
            "relationship": {
              "id": "9f951225-35c9-48c3-9169-80486ddb7b7f",
              "player_id": "ddd7453f-cf22-4de3-af38-ba799bceda59",
              "pair_id": "2b54a574-3153-47a4-b2ae-d8353b68b2d6",
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            }
          }
        ]
      },
      "rating": {
        "total_winners": "1",
        "total_score": "2"
      }
    },
    {
      "pair": {
        "id": "87a33834-299a-4f51-a8be-ba9c540e0dfd",
        "is_eliminated": 0,
        "is_active": 1,
        "created_at": "2025-03-22 03:13:32",
        "updated_at": "2025-03-22 03:13:32",
        "registration_category": {
          "id": "5d840f53-ffb0-427c-8554-bfff885d6b6a",
          "name": "open",
          "description": "Libre"
        },
        "players": [
          {
            "player": {
              "id": "c2f0e46b-7d19-4598-954a-828b8f7f98b8",
              "fullname": "Alejandro Ruiz",
              "city": "Coroneo",
              "weight": "75.50",
              "height": "1.78",
              "age": 28,
              "experience": 11,
              "is_active": 1,
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            },
            "relationship": {
              "id": "6dee11bf-95ab-4777-894b-fc824918ed42",
              "player_id": "c2f0e46b-7d19-4598-954a-828b8f7f98b8",
              "pair_id": "87a33834-299a-4f51-a8be-ba9c540e0dfd",
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            }
          },
          {
            "player": {
              "id": "e8754436-9859-4ac3-b412-f142b11d810c",
              "fullname": "Verónica Solís",
              "city": "Cortazar",
              "weight": "62.00",
              "height": "1.67",
              "age": 25,
              "experience": 5,
              "is_active": 1,
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            },
            "relationship": {
              "id": "b05f7332-8045-4209-8de1-616f3125b2cf",
              "player_id": "e8754436-9859-4ac3-b412-f142b11d810c",
              "pair_id": "87a33834-299a-4f51-a8be-ba9c540e0dfd",
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            }
          }
        ]
      },
      "rating": {
        "total_winners": "1",
        "total_score": "2"
      }
    },
    {
      "pair": {
        "id": "3f71b2f1-906c-4419-b464-666ec27ac98c",
        "is_eliminated": 0,
        "is_active": 1,
        "created_at": "2025-03-22 03:13:32",
        "updated_at": "2025-03-22 03:13:32",
        "registration_category": {
          "id": "5d840f53-ffb0-427c-8554-bfff885d6b6a",
          "name": "open",
          "description": "Libre"
        },
        "players": [
          {
            "player": {
              "id": "f93a0856-0289-435d-8e4d-e03f7e074c85",
              "fullname": "Elena Morales",
              "city": "Purísima del Rincón",
              "weight": "64.00",
              "height": "1.69",
              "age": 23,
              "experience": 5,
              "is_active": 1,
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            },
            "relationship": {
              "id": "6533cf0a-666f-4b56-bb1a-9929ea97691d",
              "player_id": "f93a0856-0289-435d-8e4d-e03f7e074c85",
              "pair_id": "3f71b2f1-906c-4419-b464-666ec27ac98c",
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            }
          },
          {
            "player": {
              "id": "5fb2f36a-76c2-4ce0-8a01-75bc48bbaf35",
              "fullname": "Diego Fernández",
              "city": "Moroleón",
              "weight": "77.00",
              "height": "1.82",
              "age": 27,
              "experience": 8,
              "is_active": 1,
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            },
            "relationship": {
              "id": "792e4c9c-0a30-4a37-890e-03ea629dd49e",
              "player_id": "5fb2f36a-76c2-4ce0-8a01-75bc48bbaf35",
              "pair_id": "3f71b2f1-906c-4419-b464-666ec27ac98c",
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            }
          }
        ]
      },
      "rating": {
        "total_winners": "0",
        "total_score": "2"
      }
    },
    {
      "pair": {
        "id": "79a230ca-16d0-47ef-8362-c6f30b95f44e",
        "is_eliminated": 0,
        "is_active": 1,
        "created_at": "2025-03-22 03:13:32",
        "updated_at": "2025-03-22 03:13:32",
        "registration_category": {
          "id": "5d840f53-ffb0-427c-8554-bfff885d6b6a",
          "name": "open",
          "description": "Libre"
        },
        "players": [
          {
            "player": {
              "id": "1dd257d2-52fd-4245-a7dd-e46f1d0db923",
              "fullname": "Enrique Castro",
              "city": "Juventino Rosas",
              "weight": "75.00",
              "height": "1.79",
              "age": 28,
              "experience": 12,
              "is_active": 1,
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            },
            "relationship": {
              "id": "00f2d827-8ae0-4b26-92d4-9479aa6897a3",
              "player_id": "1dd257d2-52fd-4245-a7dd-e46f1d0db923",
              "pair_id": "79a230ca-16d0-47ef-8362-c6f30b95f44e",
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            }
          },
          {
            "player": {
              "id": "b75e53e4-d965-4274-b640-10e0f9fc1913",
              "fullname": "Lourdes Herrera",
              "city": "Apaseo el Alto",
              "weight": "62.50",
              "height": "1.67",
              "age": 25,
              "experience": 7,
              "is_active": 1,
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            },
            "relationship": {
              "id": "56d6f1a6-d72d-410c-b27a-6af38391757c",
              "player_id": "b75e53e4-d965-4274-b640-10e0f9fc1913",
              "pair_id": "79a230ca-16d0-47ef-8362-c6f30b95f44e",
              "created_at": "2025-03-22 03:13:32",
              "updated_at": "2025-03-22 03:13:32"
            }
          }
        ]
      },
      "rating": {
        "total_winners": "0",
        "total_score": "1"
      }
    },
    ...
  ],
  "status": 200,
  "description": "Information about the registration category ranking"
}
```


code:
# 👀 Información de una categoría de inscripción

```
[GET] /v1/categories/registrations/@id
```

Parámetros de la ruta:

| Parámetros | Tipo | Requerido | Descripción |
| ---------- | ---- | --------- | ----------- |
| `id` | `string` | `true` | Identificador de la categoría de inscripción. |

Ejemplo:

```bash
curl -X GET \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'X-API-KEY: API_KEY' \
  http://localhost:8080/v1/categories/registrations/@id
```

Respuesta de la petición:

```json
{
  "data": {
    "id": "15134933-1932-4df4-bb5a-b304774b229c",
    "name": "open",
    "description": "Libre",
    "created_at": "2025-03-07 14:38:11",
    "updated_at": "2025-03-07 14:38:11"
  },
  "status": 200,
  "description": "Information about the registration category"
}
```

license: mit


code:
# 🏷 / 🧑 Roles

Roles de los usuarios de acceso.

```
[GET] /v1/roles
```

Ejemplo:

```bash
curl -X GET \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'X-API-KEY: API_KEY' \
  http://localhost:8080/v1/roles
```

Respuesta de la petición:

```json
{
  "data": [
    {
      "id": "1acc20ec-b070-4c93-be67-40dd206b2d5f",
      "name": "admin",
      "description": "Administrador",
      "created_at": "2025-03-18 14:29:06",
      "updated_at": "2025-03-18 14:29:06"
    },
    {
      "id": "c0bad9bb-092e-444b-98eb-d55b3cd1c72d",
      "name": "web",
      "description": "Sitio web",
      "created_at": "2025-03-18 14:29:06",
      "updated_at": "2025-03-18 14:29:06"
    },
    {
      "id": "61654117-62b1-445e-b258-83b807fcd25d",
      "name": "reader",
      "description": "Solo lectura",
      "created_at": "2025-03-18 14:29:06",
      "updated_at": "2025-03-18 14:29:06"
    },
    {
      "id": "f30b421c-8e30-4bf8-9984-64c3ecef85d0",
      "name": "judge",
      "description": "Juez",
      "created_at": "2025-03-18 14:29:06",
      "updated_at": "2025-03-18 14:29:06"
    }
  ],
  "status": 200,
  "description": "Information about all the access users roles"
}
```


code:
# 👀 Información de un rol

```
[GET] /v1/roles/@id
```

Parámetros de la ruta:

| Parámetros | Tipo | Requerido | Descripción |
| ---------- | ---- | --------- | ----------- |
| `id` | `string` | `true` | Identificador del rol. |

Ejemplo:

```bash
curl -X GET \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'X-API-KEY: API_KEY' \
  http://localhost:8080/v1/roles/@id
```

Respuesta de la petición:

```json
{
  "data": {
    "id": "ff23a6f7-0862-4431-b3b3-015beae0cb9f",
    "name": "admin",
    "description": "Administrador",
    "created_at": "2025-03-07 14:38:11",
    "updated_at": "2025-03-07 14:38:11"
  },
  "status": 200,
  "description": "Information about the access user role"
}
```


code:
# 🏆 Acciones de las rondas

Ejecuta acciones para las rondas del torneo.


code:
# 🎲 Asignar grupos, parejas y partidos

```
[POST] /v1/rounds/init
```

> Realiza la acción una única vez para las [categorías de inscripción](../registration-categories/index.html) que no contengan grupos asignados.

Ejemplo:

```bash
curl -X POST \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  http://localhost:8080/v1/rounds/init
```

Respuesta de la petición:

```json
{
  "data": null,
  "status": 200,
  "description": "The round init action was executed successfully"
}
```


code:
# ❌ + 👥 Descalificar grupos si todas sus parejas están descalificadas

```
[POST] /v1/rounds/purge
```

Ejemplo:

```bash
curl -X POST \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  http://localhost:8080/v1/rounds/purge/groups
```

Respuesta de la petición:

```json
{
  "data": null,
  "status": 200,
  "description": "The purge groups action was executed successfully"
}
```


code:
# ➕ Crear un usuario

```
[POST] /v1/users
```

Cuerpo de la petición:

| Campo | Tipo | Requerido | Rango | Descripción |
| ----- | ---- | --------- | ----- | ----------- |
| `fullname` | `string` | `true` | `between_len: [1, 128]` | Nombre completo del usuario. |
| `email` | `string` | `true` | `between_len: [1, 128]` | Email del usuario. |
| `username` | `string` | `true` | `between_len: [1, 64]` | Apodo del usuario. |
| `role_id` | `string` | `true` | `exact_len: 36` | Identificador del rol del usuario ([ver](../roles/index.html)). |

Ejemplo:

```bash
curl -X POST \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'X-API-KEY: API_KEY' \
  http://localhost:8080/v1/users \
  -d '{
    "fullname": "Ricardo García Jiménez",
    "email": "ricardog08@riseup.net",
    "username": "ricardog08",
    "role_id": "3d2e95aa-0616-11f0-a401-142b9a82bd2f"
  }'
```

Respuesta de la petición:

```json
{
  "data": {
    "id": "8a168aa8-02ae-4ccd-96a6-c9b5780a223e",
    "email": "ricardog08@riseup.net",
    "username": "ricardog08",
    "fullname": "Ricardo García Jiménez",
    "is_active": 1,
    "created_at": "2025-03-21 00:42:11",
    "updated_at": "2025-03-21 00:42:11",
    "role": {
      "id": "3d2e95aa-0616-11f0-a401-142b9a82bd2f",
      "name": "judge",
      "description": "Juez"
    }
  },
  "status": 200,
  "description": "Information about the user"
}
```


code:
# 👀 Información de un usuario

```
[GET] /v1/users/@id
```

Parámetros de la ruta:

| Parámetros | Tipo | Requerido | Descripción |
| ---------- | ---- | --------- | ----------- |
| `id` | `string` | `true` | Identificador del usuario. |

Ejemplo:

```bash
curl -X GET \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'X-API-KEY: API_KEY' \
  http://localhost:8080/v1/users/@id
```

Respuesta de la petición:

```json
{
  "data": {
    "id": "8a168aa8-02ae-4ccd-96a6-c9b5780a223e",
    "email": "ricardog08@riseup.net",
    "username": "ricardog08",
    "fullname": "Ricardo García Jiménez",
    "is_active": 1,
    "created_at": "2025-03-21 00:42:11",
    "updated_at": "2025-03-21 00:42:11",
    "role": {
      "id": "3d2e95aa-0616-11f0-a401-142b9a82bd2f",
      "name": "judge",
      "description": "Juez"
    }
  },
  "status": 200,
  "description": "Information about the user"
}
```


code:
#---------------------------
# APPLICATION CONFIGURATION
#---------------------------
APP_ENVIRONMENT=production
APP_BASE_URL=/
APP_TIMEZONE=America/Mexico_City
APP_SECRET=''
APP_DATA_MODEL_URL=http://localhost:3000

#------------------------
# DATABASE CONFIGURATION
#------------------------
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=torneo_fresas
DB_USERNAME=TDLF2025
DB_PASSWORD=secret
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_general_ci


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla de "jugadores".
 */
final class CreatePlayersTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('players', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('fullname', 'string', ['length' => 128])
            ->addColumn('city', 'string', ['length' => 128])
            ->addColumn('weight', 'decimal', ['signed' => false, 'length' => 5, 'decimals' => 2])
            ->addColumn('height', 'decimal', ['signed' => false, 'length' => 3, 'decimals' => 2])
            ->addColumn('age', 'tinyinteger', ['signed' => false])
            ->addColumn('experience', 'tinyinteger', ['signed' => false, 'default' => 0])
            ->addColumn('is_active', 'tinyinteger', ['signed' => false, 'length' => 1, 'default' => 1])
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->create();
    }

    protected function down(): void
    {
        $this->table('players')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla
 * de las "categorías de inscripción de las parejas de jugadores".
 */
final class CreateRegistrationCategoriesTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('registration_categories', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('name', 'string', ['length' => 32])
            ->addColumn('description', 'string', ['length' => 64])
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addUniqueConstraint('name', 'registration_categories_name_unique')
            ->create();
    }

    protected function down(): void
    {
        $this->table('registration_categories')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Database\Element\ForeignKey;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla pivote
 * de las "parejas de jugadores".
 */
final class CreatePairsTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('pairs', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('registration_category_id', 'uuid')
            ->addColumn('is_eliminated', 'tinyinteger', ['signed' => false, 'length' => 1, 'default' => 0])
            ->addColumn('is_active', 'tinyinteger', ['signed' => false, 'length' => 1, 'default' => 1])
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addForeignKey('registration_category_id', 'registration_categories', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->create();
    }

    protected function down(): void
    {
        $this->table('pairs')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Database\Element\ForeignKey;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla pivote
 * de los "jugadores de las parejas".
 */
final class CreatePairsPlayersTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('pairs_players', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('player_id', 'uuid')
            ->addColumn('pair_id', 'uuid')
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addForeignKey('player_id', 'players', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->addForeignKey('pair_id', 'pairs', 'id', ForeignKey::CASCADE, ForeignKey::RESTRICT)
            ->addUniqueConstraint('player_id', 'pairs_players_player_id_unique')
            ->create();
    }

    protected function down(): void
    {
        $this->table('pairs_players')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Database\Element\ForeignKey;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla
 * de los "grupos de las parejas de los jugadores".
 */
final class CreateGroupsTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('groups', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('registration_category_id', 'uuid')
            ->addColumn('name', 'string', ['length' => 32])
            ->addColumn('description', 'string', ['length' => 64])
            ->addColumn('is_eliminated', 'tinyinteger', ['signed' => false, 'length' => 1, 'default' => 0])
            ->addColumn('is_active', 'tinyinteger', ['signed' => false, 'length' => 1, 'default' => 1])
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addForeignKey('registration_category_id', 'registration_categories', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->addUniqueConstraint(['registration_category_id', 'name'], 'groups_registration_category_id_name_unique')
            ->create();
    }

    protected function down(): void
    {
        $this->table('groups')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Database\Element\ForeignKey;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla pivote
 * de las "parejas de los grupos de jugadores".
 */
final class CreateGroupsPairsTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('groups_pairs', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('pair_id', 'uuid')
            ->addColumn('group_id', 'uuid')
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addForeignKey('pair_id', 'pairs', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->addForeignKey('group_id', 'groups', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->addUniqueConstraint('pair_id', 'groups_pairs_pair_id_unique')
            ->create();
    }

    protected function down(): void
    {
        $this->table('groups_pairs')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla de las "categorías de los partidos".
 */
final class CreateMatchCategoryTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('match_categories', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('name', 'string', ['length' => 32])
            ->addColumn('description', 'string', ['length' => 64])
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addUniqueConstraint('name', 'match_categories_name_unique')
            ->create();
    }

    protected function down(): void
    {
        $this->table('match_categories')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla
 * de los "estatus de juego de los partidos".
 */
final class CreateMatchStatusTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('match_status', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('name', 'string', ['length' => 32])
            ->addColumn('description', 'string', ['length' => 64])
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addUniqueConstraint('name', 'match_status_name_unique')
            ->create();
    }

    protected function down(): void
    {
        $this->table('match_status')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Database\Element\ForeignKey;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla de los "partidos".
 */
final class CreateMatchesTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('matches', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('registration_category_id', 'uuid')
            ->addColumn('match_category_id', 'uuid')
            ->addColumn('match_status_id', 'uuid')
            ->addColumn('is_active', 'tinyinteger', ['signed' => false, 'length' => 1, 'default' => 1])
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addForeignKey('registration_category_id', 'registration_categories', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->addForeignKey('match_category_id', 'match_categories', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->addForeignKey('match_status_id', 'match_status', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->create();
    }

    protected function down(): void
    {
        $this->table('matches')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Database\Element\ForeignKey;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla pivote
 * de los "partidos de las parejas de jugadores".
 */
final class CreateMatchesPairsTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('matches_pairs', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('pair_id', 'uuid')
            ->addColumn('match_id', 'uuid')
            ->addColumn('score', 'tinyinteger', ['signed' => false, 'default' => 0])
            ->addColumn('is_winner', 'tinyinteger', ['signed' => false, 'length' => 1, 'default' => 0])
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addForeignKey('pair_id', 'pairs', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->addForeignKey('match_id', 'matches', 'id', ForeignKey::CASCADE, ForeignKey::RESTRICT)
            ->addUniqueConstraint(['pair_id', 'match_id'], 'matches_pairs_pair_id_match_id_unique')
            ->create();
    }

    protected function down(): void
    {
        $this->table('matches_pairs')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla
 * de los "roles de los usuarios de acceso".
 */
final class CreateRolesTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('roles', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('name', 'string', ['length' => 32])
            ->addColumn('description', 'string', ['length' => 64])
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addUniqueConstraint('name', 'roles_name_unique')
            ->create();
    }

    protected function down(): void
    {
        $this->table('roles')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Database\Element\ForeignKey;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla
 * de los "usuarios de acceso".
 */
final class CreateUsersTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('users', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('email', 'string', ['length' => 128])
            ->addColumn('username', 'string', ['length' => 64])
            ->addColumn('role_id', 'uuid')
            ->addColumn('fullname', 'string', ['length' => 128])
            ->addColumn('password', 'string', ['length' => 255])
            ->addColumn('is_active', 'tinyinteger', ['signed' => false, 'length' => 1, 'default' => 1])
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addForeignKey('role_id', 'roles', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->addUniqueConstraint('email', 'users_email_unique')
            ->addUniqueConstraint('username', 'users_username_unique')
            ->create();
    }

    protected function down(): void
    {
        $this->table('users')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use App\Helpers\Database;
use App\Helpers\Date;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que inserta información en la tabla
 * de las "categorías de inscripción de las parejas de jugadores".
 */
final class SeederRegistrationCategoriesTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->insert('registration_categories', [
            [
                'id' => Database::getUuid(),
                'name' => 'open',
                'description' => 'Libre',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'name' => 'seniors',
                'description' => '50 y más',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
        ]);
    }

    protected function down(): void
    {
        $this->delete('matches');
        $this->delete('groups_pairs');
        $this->delete('groups');
        $this->delete('pairs');
        $this->delete('registration_categories');
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use App\Helpers\Database;
use App\Helpers\Date;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que inserta información en la tabla
 * de los "roles de los usuarios de acceso".
 */
final class SeederRolesTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->insert('roles', [
            [
                'id' => Database::getUuid(),
                'name' => 'admin',
                'description' => 'Administrador',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'name' => 'web',
                'description' => 'Sitio web',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'name' => 'reader',
                'description' => 'Solo lectura',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'name' => 'judge',
                'description' => 'Juez',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
        ]);
    }

    protected function down(): void
    {
        $this->delete('users');
        $this->delete('roles');
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Database\Element\ForeignKey;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla
 * de las "API keys de los usuarios de acceso".
 */
final class CreateApiKeysTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('api_keys', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('user_id', 'uuid')
            ->addColumn('hash', 'string', ['length' => 255])
            ->addColumn('is_revoked', 'tinyinteger', ['signed' => false, 'length' => 1, 'default' => 0])
            ->addColumn('expires_at', 'timestamp')
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addUniqueConstraint('user_id', 'api_keys_user_id_unique')
            ->addForeignKey('user_id', 'users', 'id', ForeignKey::CASCADE, ForeignKey::RESTRICT)
            ->create();
    }

    protected function down(): void
    {
        $this->table('api_keys')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use App\Helpers\Database;
use App\Helpers\Date;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que inserta la información en la tabla
 * de los "usuarios de acceso".
 */
final class SeederUsersTable extends AbstractMigration
{
    protected function up(): void
    {
        $roles = $this->select('SELECT id, name FROM roles');

        $find_role = static function (string $name) use ($roles): ?array {
            foreach ($roles as $role) {
                if ($role['name'] === $name) {
                    return $role;
                }
            }

            return null;
        };

        $this->insert('users', [
            [
                'id' => Database::getUuid(),
                'email' => 'franciscosolism08@gmail.com',
                'username' => 'ezio',
                'role_id' => $find_role('web')['id'],
                'fullname' => 'Francisco Solís Martínez',
                'password' => '$2y$10$qPOISeg/nx9jz8MNbsvpNOCNWnH22yCTrBlOC4DRCwUYVMKW15DUi',
                'is_active' => 1,
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'email' => 'juan.ramirez.j99@gmail.com',
                'username' => 'astra',
                'role_id' => $find_role('reader')['id'],
                'fullname' => 'Juan José Ramírez López',
                'password' => '$2y$10$okFmKNpgJp3GVqi6oMUW9u0A6Dw1s9ppyEsR25taq82S/cKuM9Vx6',
                'is_active' => 1,
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'email' => 'ricardogj08@riseup.net',
                'username' => 'ricardogj08',
                'role_id' => $find_role('admin')['id'],
                'fullname' => 'Ricardo García Jiménez',
                'password' => '$2y$10$cyBfiRA38HsO5UrasH1jkOTEozY7irgDZD3Ah1qzXTRDeRMY8PKa2',
                'is_active' => 1,
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
        ]);
    }

    protected function down(): void
    {
        $this->delete('users');
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use App\Helpers\Database;
use App\Helpers\Date;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que inserta la información en la tabla
 * de las "categorías de los partidos".
 */
final class SeederMatchCategoriesTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->insert('match_categories', [
            [
                'id' => Database::getUuid(),
                'name' => 'qualifier',
                'description' => 'Clasificación',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'name' => 'eighths',
                'description' => 'Octavos de final',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'name' => 'quarters',
                'description' => 'Cuartos de final',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'name' => 'semifinal',
                'description' => 'Semifinal',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'name' => 'final',
                'description' => 'Final',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
        ]);
    }

    protected function down(): void
    {
        $this->delete('matches');
        $this->delete('match_categories');
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use App\Helpers\Database;
use App\Helpers\Date;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que inserta la información en la tabla
 * de los "estatus de juego de los partidos".
 */
final class SeederMatchStatusTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->insert('match_status', [
            [
                'id' => Database::getUuid(),
                'name' => 'scheduled',
                'description' => 'Programado',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'name' => 'playing',
                'description' => 'En juego',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
            [
                'id' => Database::getUuid(),
                'name' => 'finalized',
                'description' => 'Finalizado',
                'created_at' => Date::getCurrentDateTime(),
                'updated_at' => Date::getCurrentDateTime(),
            ],
        ]);
    }

    protected function down(): void
    {
        $this->delete('matches');
        $this->delete('match_status');
    }
}


code:
<?php

declare(strict_types=1);

namespace FirstDir;

use Phoenix\Database\Element\ColumnSettings;
use Phoenix\Database\Element\ForeignKey;
use Phoenix\Migration\AbstractMigration;

/**
 * Migración que crea la tabla pivote
 * de los "partidos" de los "grupos".
 */
final class CreateGroupsMatchesTable extends AbstractMigration
{
    protected function up(): void
    {
        $this->table('groups_matches', 'id')
            ->addColumn('id', 'uuid')
            ->addColumn('group_id', 'uuid')
            ->addColumn('match_id', 'uuid')
            ->addColumn('created_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addColumn('updated_at', 'timestamp', ['default' => ColumnSettings::DEFAULT_VALUE_CURRENT_TIMESTAMP])
            ->addForeignKey('group_id', 'groups', 'id', ForeignKey::RESTRICT, ForeignKey::RESTRICT)
            ->addForeignKey('match_id', 'matches', 'id', ForeignKey::CASCADE, ForeignKey::RESTRICT)
            ->addUniqueConstraint(['group_id', 'match_id'], 'groups_matches_group_id_match_id_unique')
            ->create();
    }

    protected function down(): void
    {
        $this->table('groups_matches')->drop();
    }
}


code:
<?php

declare(strict_types=1);

namespace SecondDir;

use App\Helpers\Database;
use App\Helpers\Date;
use Phoenix\Migration\AbstractMigration;

final class SeederPairsPlayersTable extends AbstractMigration
{
    protected function up(): void
    {
        $categories = $this->select('SELECT id, name FROM registration_categories');

        $find_category = static function (string $name) use ($categories): ?array {
            foreach ($categories as $category) {
                if ($category['name'] === $name) {
                    return $category;
                }
            }

            return null;
        };

        $universe = $this->getUniverse();

        // Itera sobre las parejas de cada categoría.
        foreach ($universe as $categoryName => $pairs) {
            $category = $find_category($categoryName);

            // Itera sobre todas las parejas de la categoría.
            foreach ($pairs as $players) {
                $pairId = Database::getUuid();

                // Registra una nueva pareja.
                $this->insert('pairs', [
                    'id' => $pairId,
                    'registration_category_id' => $category['id'],
                    'is_eliminated' => 0,
                    'is_active' => 1,
                    'created_at' => Date::getCurrentDateTime(),
                    'updated_at' => Date::getCurrentDateTime(),
                ]);

                // Itera sobre los jugadores de la pareja.
                foreach ($players as $player) {
                    $playerId = Database::getUuid();

                    // Registra un nuevo jugador.
                    $this->insert('players', array_merge([
                        'id' => $playerId,
                        'is_active' => 1,
                        'created_at' => Date::getCurrentDateTime(),
                        'updated_at' => Date::getCurrentDateTime(),
                    ], $player));

                    // Registra la relación del jugador con la pareja.
                    $this->insert('pairs_players', [
                        'id' => Database::getUuid(),
                        'pair_id' => $pairId,
                        'player_id' => $playerId,
                        'created_at' => Date::getCurrentDateTime(),
                        'updated_at' => Date::getCurrentDateTime(),
                    ]);
                }
            }
        }
    }

    protected function down(): void
    {
        $this->delete('matches');
        $this->delete('groups_pairs');
        $this->delete('groups');
        $this->delete('pairs');
        $this->delete('players');
    }

    private function getUniverse()
    {
        return [
            'open' => [
                [
                    ['fullname' => 'Luis Hernández', 'city' => 'León', 'weight' => 75.20, 'height' => 1.78, 'age' => 28, 'experience' => 12],
                    ['fullname' => 'María Guzmán', 'city' => 'Guanajuato', 'weight' => 62.50, 'height' => 1.68, 'age' => 24, 'experience' => 8],
                ],
                [
                    ['fullname' => 'Carlos Martínez', 'city' => 'Celaya', 'weight' => 78.40, 'height' => 1.80, 'age' => 32, 'experience' => 10],
                    ['fullname' => 'Ana Ramírez', 'city' => 'Irapuato', 'weight' => 63.10, 'height' => 1.65, 'age' => 26, 'experience' => 7],
                ],
                [
                    ['fullname' => 'Javier Torres', 'city' => 'Salamanca', 'weight' => 80.00, 'height' => 1.85, 'age' => 35, 'experience' => 15],
                    ['fullname' => 'Sofía López', 'city' => 'San Miguel de Allende', 'weight' => 65.00, 'height' => 1.70, 'age' => 29, 'experience' => 9],
                ],
                [
                    ['fullname' => 'Pedro Gómez', 'city' => 'Silao', 'weight' => 72.50, 'height' => 1.76, 'age' => 30, 'experience' => 11],
                    ['fullname' => 'Laura Vargas', 'city' => 'Dolores Hidalgo', 'weight' => 60.50, 'height' => 1.67, 'age' => 25, 'experience' => 6],
                ],
                [
                    ['fullname' => 'Diego Fernández', 'city' => 'Moroleón', 'weight' => 77.00, 'height' => 1.82, 'age' => 27, 'experience' => 8],
                    ['fullname' => 'Elena Morales', 'city' => 'Purísima del Rincón', 'weight' => 64.00, 'height' => 1.69, 'age' => 23, 'experience' => 5],
                ],
                [
                    ['fullname' => 'Miguel Ángel', 'city' => 'Apaseo el Grande', 'weight' => 79.50, 'height' => 1.84, 'age' => 33, 'experience' => 13],
                    ['fullname' => 'Gabriela Jiménez', 'city' => 'Uriangato', 'weight' => 66.20, 'height' => 1.72, 'age' => 28, 'experience' => 10],
                ],
                [
                    ['fullname' => 'José Ramírez', 'city' => 'Yuriria', 'weight' => 74.00, 'height' => 1.79, 'age' => 31, 'experience' => 14],
                    ['fullname' => 'Valeria Castro', 'city' => 'Acámbaro', 'weight' => 61.80, 'height' => 1.66, 'age' => 22, 'experience' => 4],
                ],
                [
                    ['fullname' => 'Raúl Sánchez', 'city' => 'Pénjamo', 'weight' => 76.30, 'height' => 1.81, 'age' => 29, 'experience' => 12],
                    ['fullname' => 'Daniela Ortega', 'city' => 'Comonfort', 'weight' => 63.50, 'height' => 1.68, 'age' => 24, 'experience' => 7],
                ],
                [
                    ['fullname' => 'Fernando Guerra', 'city' => 'Villagrán', 'weight' => 73.00, 'height' => 1.77, 'age' => 30, 'experience' => 9],
                    ['fullname' => 'Isabel Pérez', 'city' => 'Abasolo', 'weight' => 60.00, 'height' => 1.65, 'age' => 26, 'experience' => 6],
                ],
                [
                    ['fullname' => 'Roberto Díaz', 'city' => 'Jerécuaro', 'weight' => 78.00, 'height' => 1.83, 'age' => 34, 'experience' => 13],
                    ['fullname' => 'Claudia Mendoza', 'city' => 'Tarimoro', 'weight' => 64.50, 'height' => 1.70, 'age' => 27, 'experience' => 8],
                ],
                [
                    ['fullname' => 'Alejandro Ruiz', 'city' => 'Coroneo', 'weight' => 75.50, 'height' => 1.78, 'age' => 28, 'experience' => 11],
                    ['fullname' => 'Verónica Solís', 'city' => 'Cortazar', 'weight' => 62.00, 'height' => 1.67, 'age' => 25, 'experience' => 5],
                ],
                [
                    ['fullname' => 'Héctor Márquez', 'city' => 'Pueblo Nuevo', 'weight' => 79.00, 'height' => 1.84, 'age' => 36, 'experience' => 15],
                    ['fullname' => 'Patricia Navarro', 'city' => 'Santa Cruz de Juventino Rosas', 'weight' => 65.50, 'height' => 1.71, 'age' => 30, 'experience' => 10],
                ],
                [
                    ['fullname' => 'Ricardo Silva', 'city' => 'Doctor Mora', 'weight' => 74.50, 'height' => 1.80, 'age' => 32, 'experience' => 12],
                    ['fullname' => 'Karla Rivas', 'city' => 'Victoria', 'weight' => 63.00, 'height' => 1.68, 'age' => 27, 'experience' => 7],
                ],
                [
                    ['fullname' => 'Manuel Aguilar', 'city' => 'Xichú', 'weight' => 76.00, 'height' => 1.82, 'age' => 31, 'experience' => 10],
                    ['fullname' => 'Natalia Vega', 'city' => 'Atarjea', 'weight' => 64.20, 'height' => 1.69, 'age' => 26, 'experience' => 6],
                ],
                [
                    ['fullname' => 'Francisco Núñez', 'city' => 'San Felipe', 'weight' => 73.50, 'height' => 1.77, 'age' => 29, 'experience' => 9],
                    ['fullname' => 'Adriana Flores', 'city' => 'Ocampo', 'weight' => 60.80, 'height' => 1.66, 'age' => 24, 'experience' => 5],
                ],
                [
                    ['fullname' => 'Alberto Méndez', 'city' => 'San José Iturbide', 'weight' => 77.50, 'height' => 1.83, 'age' => 33, 'experience' => 13],
                    ['fullname' => 'Martha González', 'city' => 'Tierra Blanca', 'weight' => 65.00, 'height' => 1.70, 'age' => 28, 'experience' => 8],
                ],
                [
                    ['fullname' => 'Arturo López', 'city' => 'Romita', 'weight' => 78.50, 'height' => 1.84, 'age' => 35, 'experience' => 14],
                    ['fullname' => 'Rebeca Torres', 'city' => 'Purísima del Rincón', 'weight' => 66.00, 'height' => 1.72, 'age' => 30, 'experience' => 11],
                ],
                [
                    ['fullname' => 'Enrique Castro', 'city' => 'Juventino Rosas', 'weight' => 75.00, 'height' => 1.79, 'age' => 28, 'experience' => 12],
                    ['fullname' => 'Lourdes Herrera', 'city' => 'Apaseo el Alto', 'weight' => 62.50, 'height' => 1.67, 'age' => 25, 'experience' => 7],
                ],
                [
                    ['fullname' => 'Mario Guzmán', 'city' => 'Salvatierra', 'weight' => 79.20, 'height' => 1.81, 'age' => 34, 'experience' => 15],
                    ['fullname' => 'Sandra Paredes', 'city' => 'Jaral del Progreso', 'weight' => 64.80, 'height' => 1.68, 'age' => 27, 'experience' => 9],
                ],
                [
                    ['fullname' => 'Rodrigo Vázquez', 'city' => 'Cuerámaro', 'weight' => 74.80, 'height' => 1.78, 'age' => 30, 'experience' => 11],
                    ['fullname' => 'Miriam Alvarado', 'city' => 'Huanímaro', 'weight' => 63.00, 'height' => 1.66, 'age' => 26, 'experience' => 6],
                ],
                [
                    ['fullname' => 'Jorge Medina', 'city' => 'Pueblo Nuevo', 'weight' => 76.50, 'height' => 1.82, 'age' => 32, 'experience' => 13],
                    ['fullname' => 'Maricela Sánchez', 'city' => 'Moroleón', 'weight' => 65.20, 'height' => 1.70, 'age' => 29, 'experience' => 8],
                ],
                [
                    ['fullname' => 'Felipe Ramírez', 'city' => 'Yuriria', 'weight' => 73.80, 'height' => 1.77, 'age' => 29, 'experience' => 10],
                    ['fullname' => 'Carmen Mendoza', 'city' => 'Acámbaro', 'weight' => 61.50, 'height' => 1.65, 'age' => 24, 'experience' => 5],
                ],
                [
                    ['fullname' => 'Daniel Guerra', 'city' => 'Pénjamo', 'weight' => 78.00, 'height' => 1.83, 'age' => 33, 'experience' => 12],
                    ['fullname' => 'Leticia Ortega', 'city' => 'Comonfort', 'weight' => 63.50, 'height' => 1.68, 'age' => 25, 'experience' => 7],
                ],
                [
                    ['fullname' => 'Samuel Sánchez', 'city' => 'Villagrán', 'weight' => 75.00, 'height' => 1.79, 'age' => 28, 'experience' => 11],
                    ['fullname' => 'Rosa Pérez', 'city' => 'Abasolo', 'weight' => 60.00, 'height' => 1.66, 'age' => 23, 'experience' => 6],
                ],
            ],
            'seniors' => [
                [
                    ['fullname' => 'Antonio López', 'city' => 'León', 'weight' => 78.50, 'height' => 1.72, 'age' => 55, 'experience' => 25],
                    ['fullname' => 'Carmen Ramírez', 'city' => 'Guanajuato', 'weight' => 65.00, 'height' => 1.60, 'age' => 60, 'experience' => 30],
                ],
                [
                    ['fullname' => 'Jorge Hernández', 'city' => 'Celaya', 'weight' => 80.00, 'height' => 1.75, 'age' => 58, 'experience' => 28],
                    ['fullname' => 'María Guzmán', 'city' => 'Irapuato', 'weight' => 67.50, 'height' => 1.62, 'age' => 52, 'experience' => 22],
                ],
                [
                    ['fullname' => 'Roberto Torres', 'city' => 'Salamanca', 'weight' => 79.00, 'height' => 1.73, 'age' => 65, 'experience' => 35],
                    ['fullname' => 'Sofía Mendoza', 'city' => 'San Miguel de Allende', 'weight' => 66.00, 'height' => 1.61, 'age' => 57, 'experience' => 27],
                ],
                [
                    ['fullname' => 'Carlos Sánchez', 'city' => 'Silao', 'weight' => 77.50, 'height' => 1.74, 'age' => 54, 'experience' => 24],
                    ['fullname' => 'Laura Vargas', 'city' => 'Dolores Hidalgo', 'weight' => 64.50, 'height' => 1.63, 'age' => 59, 'experience' => 29],
                ],
                [
                    ['fullname' => 'Pedro Martínez', 'city' => 'Moroleón', 'weight' => 76.00, 'height' => 1.70, 'age' => 62, 'experience' => 32],
                    ['fullname' => 'Elena Morales', 'city' => 'Purísima del Rincón', 'weight' => 63.00, 'height' => 1.60, 'age' => 56, 'experience' => 26],
                ],
                [
                    ['fullname' => 'Fernando Gómez', 'city' => 'Apaseo el Grande', 'weight' => 78.20, 'height' => 1.71, 'age' => 53, 'experience' => 23],
                    ['fullname' => 'Gabriela Jiménez', 'city' => 'Uriangato', 'weight' => 65.50, 'height' => 1.62, 'age' => 61, 'experience' => 31],
                ],
                [
                    ['fullname' => 'José Ramírez', 'city' => 'Yuriria', 'weight' => 79.50, 'height' => 1.76, 'age' => 64, 'experience' => 34],
                    ['fullname' => 'Valeria Castro', 'city' => 'Acámbaro', 'weight' => 66.80, 'height' => 1.64, 'age' => 58, 'experience' => 28],
                ],
                [
                    ['fullname' => 'Raúl Sánchez', 'city' => 'Pénjamo', 'weight' => 77.00, 'height' => 1.72, 'age' => 57, 'experience' => 27],
                    ['fullname' => 'Daniela Ortega', 'city' => 'Comonfort', 'weight' => 64.00, 'height' => 1.60, 'age' => 55, 'experience' => 25],
                ],
                [
                    ['fullname' => 'Fernando Guerra', 'city' => 'Villagrán', 'weight' => 76.50, 'height' => 1.70, 'age' => 63, 'experience' => 33],
                    ['fullname' => 'Isabel Pérez', 'city' => 'Abasolo', 'weight' => 63.50, 'height' => 1.61, 'age' => 59, 'experience' => 29],
                ],
                [
                    ['fullname' => 'Roberto Díaz', 'city' => 'Jerécuaro', 'weight' => 78.00, 'height' => 1.73, 'age' => 56, 'experience' => 26],
                    ['fullname' => 'Claudia Mendoza', 'city' => 'Tarimoro', 'weight' => 65.20, 'height' => 1.62, 'age' => 60, 'experience' => 30],
                ],
                [
                    ['fullname' => 'Alejandro Ruiz', 'city' => 'Coroneo', 'weight' => 77.80, 'height' => 1.74, 'age' => 54, 'experience' => 24],
                    ['fullname' => 'Verónica Solís', 'city' => 'Cortazar', 'weight' => 64.80, 'height' => 1.63, 'age' => 58, 'experience' => 28],
                ],
                [
                    ['fullname' => 'Héctor Márquez', 'city' => 'Pueblo Nuevo', 'weight' => 79.20, 'height' => 1.75, 'age' => 61, 'experience' => 31],
                    ['fullname' => 'Patricia Navarro', 'city' => 'Santa Cruz de Juventino Rosas', 'weight' => 66.50, 'height' => 1.64, 'age' => 57, 'experience' => 27],
                ],
            ],
        ];

    }
}


code:
<?php

declare(strict_types=1);

require __DIR__.'/vendor/autoload.php';

/**
 * Opciones de configuración de las migraciones de la base de datos.
 */
return (static function (): array {
    // Carga variables de entorno desde el archivo ".env".
    \App\Helpers\Env::loadDotEnv();

    // Carga opciones de configuración de la aplicación.
    \App\Helpers\Config::getFromFilename('app');

    // Obtiene las opciones de configuración de la base de datos.
    $options = \App\Helpers\Config::getFromFilename('database');

    // Establece los directorios de las migraciones.
    $directories = ['first' => __DIR__.'/migrations/first_dir'];

    // Agrega el directorio de las migraciones de desarrollo.
    if (\App\Helpers\Env::get('APP_ENVIRONMENT') === 'development') {
        $directories['second'] = __DIR__.'/migrations/second_dir';
    }

    return [
        'log_table_name' => 'phoenix_log',
        'migration_dirs' => $directories,
        'environments' => [
            'production' => [
                'adapter' => $options['driver'],
                'host' => $options['host'],
                'port' => $options['port'],
                'username' => $options['username'],
                'password' => $options['password'],
                'db_name' => $options['database'],
                'charset' => $options['charset'],
                'collation' => $options['collation'],
            ],
        ],
        'default_environment ' => 'production',
        'indent' => '4spaces',
    ];
})();


code:
RewriteEngine On
#RewriteBase /subdir/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]


code:
<?php

require __DIR__.'/../config/bootstrap.php';


code:
SWIKI_OUTPUT=../docs/api
SWIKI_SOURCE=./docs
SWIKI_URL=https://astra-exe.github.io/TDLF2025/api/
# SWIKI_URL=$(dirname "$PWD")/docs/api
SWIKI_HOME="Inicio"
SWIKI_LANG=es
SWIKI_LEFT_NAVBAR=""
SWIKI_RIGHT_NAVBAR="[Repositorio](https://github.com/Astra-exe/TDLF2025/tree/main/api) [Contacto](mailto:ricardogj08@riseup.net)"
SWIKI_LICENSE="© CC-BY-4.0"
SWIKI_FAVICON="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>📘</text></svg>"


code:
API_NICKNAME = "astra"
API_PASSWORD = "passwordsecureexample"
CATHEGORY_SENIORS_ID = 'id'
CATHEGORY_OPEN_ID = 'id'


code:
#Folders
venv
__pycache__

#Files
.env
players2.json
coords_cache.json


code:
FROM python:3.12-slim

WORKDIR /app

# Instalar dependencias del sistema para compilar pandas
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    libxml2-dev \
    libxslt-dev \
    && rm -rf /var/lib/apt/lists/*

# Copiar requirements.txt primero para cachear las dependencias
COPY da/requirements.txt .

# Instalar dependencias de Python
RUN pip install --upgrade pip && \
    pip install -r requirements.txt

# Copiar el resto del código
COPY da /app

# Puerto expuesto (Koyeb usa $PORT automáticamente)
ENV PORT=80
EXPOSE 80

# Comando de inicio
CMD ["gunicorn", "--bind", "0.0.0.0:80", "app:app"]

path: da/Dockerfile


code:
# Documentación de la API de Análisis de Datos

API construida con Flask que provee varios endpoints para generar perfiles de jugadores, mapas, gráficos de paridad, puntos y sinergia, y comparativas entre categorías. La aplicación consume datos de la API en php del torneo y los procesa de forma local. Utiliza funciones importadas desde `utils.features` para procesar y graficar los datos.

---

## Índice

- [Documentación de la API de Análisis de Datos](#documentación-de-la-api-de-análisis-de-datos)
  - [Índice](#índice)
  - [Requisitos](#requisitos)
  - [Configuración del Entorno](#configuración-del-entorno)
  - [Estructura del Proyecto](#estructura-del-proyecto)
  - [Endpoints](#endpoints)
    - [GET `/`](#get-)
    - [GET `/profile/<player_id>`](#get-profileplayer_id)
    - [GET `/map`](#get-map)
    - [GET `/parity/<cathegory_id>`](#get-paritycathegory_id)
    - [GET `/points/<cathegory_id>/<group>`](#get-pointscathegory_idgroup)
    - [GET `/sinergy/<cathegory_id>`](#get-sinergycathegory_id)
    - [GET `/sinergy_compare`](#get-sinergy_compare)
    - [GET `/points_compare`](#get-points_compare)
    - [GET `/feelings`](#get-feelings)
  - [Manejo de Errores](#manejo-de-errores)
  - [Ejecución](#ejecución)
  - [Notas Adicionales](#notas-adicionales)

---

## Requisitos

- **Python 3.11+**
- **Flask** y **Flask-CORS**
- **Requests**
- **Pandas**
- **Numpy**
- **Plotly** (a través de las funciones importadas desde `utils.features`)
- **python-dotenv** para cargar variables de entorno

---

## Configuración del Entorno

La aplicación utiliza variables de entorno definidas en un archivo `.env`. Asegúrate de tener configuradas las siguientes variables:

- `API_NICKNAME`: Nickname para autenticación.
- `API_PASSWORD`: Contraseña para autenticación.
- `CATHEGORY_SENIORS_ID`: Identificador de la categoría para seniors.
- `CATHEGORY_OPEN_ID`: Identificador de la categoría para la categoría abierta.

Estas variables se cargan al inicio de la aplicación mediante el paquete `dotenv`:

```python
from dotenv import load_dotenv
load_dotenv()
```

---

## Estructura del Proyecto

```
.
├── app.py                 # Archivo principal (el código mostrado)
├── .env                   # Archivo de configuración de variables de entorno
├── templates/
│   └── welcome.html       # Plantilla para la ruta de inicio
├── players_location.json  # Archivo JSON con la ubicación de los jugadores
├── parity_seniors.json    # Archivo JSON con datos de paridad para seniors
├── parity_open.json       # Archivo JSON con datos de paridad para categoría abierta
├── data_matches_seniors.csv   # Archivo CSV con datos de partidos de seniors
├── data_matches_open.csv      # Archivo CSV con datos de partidos de categoría abierta
└── utils/
    └── features.py        # Funciones de procesamiento y generación de gráficos
```

---

## Endpoints

### GET `/`
- **Descripción:** Renderiza la plantilla `welcome.html`.
- **Método:** `GET`
- **Salida:** Página HTML.

**Ejemplo de uso:**
```sh
curl -X GET http://localhost:3000/
```

### GET `/profile/<player_id>`
- **Descripción:** Obtiene datos del jugador usando el `player_id` y genera un perfil.
- **Parámetros URL:** `player_id`.
- **Método:** `GET`
- **Salida:** Respuesta JSON con el perfil generado o mensaje de error.

**Ejemplo de uso:**
```sh
curl -X GET http://localhost:3000/profile/eeaaa56a564aa
```

### GET `/map`
- **Descripción:** Genera un mapa de ubicación de los jugadores.
- **Método:** `GET`
- **Salida:** Respuesta JSON con el HTML del mapa.

**Ejemplo de uso:**
```sh
curl -X GET http://localhost:3000/map
```

### GET `/parity/<cathegory_id>`
- **Descripción:** Genera un gráfico de paridad basado en la categoría.
- **Parámetros URL:** `cathegory_id`.
- **Método:** `GET`
- **Salida:** Respuesta JSON con los datos del gráfico.

**Ejemplo de uso:**
```sh
curl -X GET http://localhost:3000/parity/aacace26-65b6-4ac9-8e7c-fcb32061c3fd
```

### GET `/points/<cathegory_id>/<group>`
- **Descripción:** Genera un gráfico de puntos basado en la categoría y grupo.
- **Parámetros URL:** `cathegory_id`, `group`.
- **Método:** `GET`
- **Salida:** Respuesta JSON con datos del gráfico.

**Ejemplo de uso:**
```sh
curl -X GET http://localhost:3000/points/aacace26-65b6-4ac9-8e7c-fcb32061c3fd/A
```

### GET `/sinergy/<cathegory_id>`
- **Descripción:** Genera un gráfico de sinergia basado en la categoría.
- **Parámetros URL:** `cathegory_id`.
- **Método:** `GET`
- **Salida:** Respuesta JSON con datos del gráfico.

**Ejemplo de uso:**
```sh
curl -X GET http://localhost:3000/sinergy/aacace26-65b6-4ac9-8e7c-fcb32061c3fd
```

### GET `/sinergy_compare`
- **Descripción:** Genera una comparativa de sinergia entre categorías.
- **Método:** `GET`
- **Salida:** Respuesta JSON con datos del gráfico.

**Ejemplo de uso:**
```sh
curl -X GET http://localhost:3000/sinergy_compare
```

### GET `/points_compare`
- **Descripción:** Genera una comparativa de puntos entre categorías.
- **Método:** `GET`
- **Salida:** Respuesta JSON con datos del gráfico.

### GET `/feelings`
- **Descripción:** Obtiene un análisis en formato markdown.
- **Método:** `GET`
- **Salida:** Respuesta JSON con el análisis textual en formato markdown.

**Ejemplo de uso:**
```sh
curl -X GET http://localhost:3000/feelings
```

**Ejemplo de respuesta:**
```json
{
  "analysis": "# Análisis de Sentimientos\n\nEste es un ejemplo de texto en markdown con insights sobre los datos.\n\n- Punto 1: Observación relevante.\n- Punto 2: Otra observación importante."
}
```

**Ejemplo de uso:**
```sh
curl -X GET http://localhost:3000/points_compare
```

---

## Manejo de Errores

- **HTTPError:** Se manejan errores HTTP al consumir la API externa.
- **FileNotFoundError:** Se devuelve un error 404 si falta algún archivo JSON o CSV.
- **Errores de conexión:** Se capturan excepciones de conexión y tiempos de espera.
- **Errores genéricos:** Se capturan con un código 500.

---

## Ejecución

1. Instala las dependencias:
   ```bash
   pip install -r requirements.txt
   ```
2. Configura el archivo `.env` con las variables de entorno necesarias.
3. Ejecuta la aplicación:
   ```bash
   python app.py
   ```

La aplicación se ejecutará en el puerto `3000` en modo `debug`.

---

## Notas Adicionales

- Las funciones para generar gráficos y procesar datos se importan desde `utils.features`.
- Los archivos `.csv` y `.json` deben estar en la raíz del proyecto.
- La autenticación a la API externa se realiza mediante `get_apikey()` con las credenciales del `.env`.


code:
from utils.features import (
    playerp,
    players_location,
    plotly_plot_parity,
    extract_group,
    plotly_plot_points,
    plotly_plot_sinergy,
    plotly_plot_sinergy_compare,
    plotly_plot_points_compare,
    get_analysys
)

from flask import Flask, request, jsonify, render_template
from flask_cors import CORS
import requests
import numpy as np
import pandas as pd

import os
from dotenv import load_dotenv
import json

load_dotenv()

#create the app
app = Flask(__name__)
CORS(app)

app.config["API_USER"] = os.getenv("API_NICKNAME")
app.config["API_PASSWORD"] = os.getenv("API_PASSWORD")
app.config["CATHEGORY_SENIORS_ID"] = os.getenv("CATHEGORY_SENIORS_ID")
app.config["CATHEGORY_OPEN_ID"] = os.getenv("CATHEGORY_OPEN_ID")

def get_apikey():
    payload = {
        "nickname":app.config["API_USER"],
        "password":app.config["API_PASSWORD"]
    }

    try:
        response = requests.post('https://api-x90k.onrender.com/v1/auth/login', json=payload)
        response.raise_for_status()
        data = response.json()
        data = data['data']
        return data['api_key']

    except requests.exceptions.HTTPError as err:
        print(err)
    except requests.exceptions.ConnectionError:
        print("Error de conexión")
    except requests.exceptions.Timeout:
        print("Se agotó el tiempo de la solicitud")
    except Exception as e:
        print("Error inesperado: ", str(e))

    return None
#endpoints
@app.route('/')
def home():
    return render_template('welcome.html')

@app.route('/profile/<player_id>', methods=['GET'])
def make_profile(player_id):
    # Get the API key
    api_key = get_apikey()
    if api_key is None:
        return jsonify({"error": "No se pudo obtener la Api Key para la ruta /profile"}), 400

    headers = {
        'X-API-KEY': api_key
    }

    # Get the player data
    try:
        response = requests.get(f'https://api-x90k.onrender.com/v1/players/{player_id}', headers=headers)
        response.raise_for_status()
        data = response.json()

        player_data = data['data']
        age = int(player_data['age'])
        height = float(player_data['height'])
        weight = float(player_data['weight'])

        # Generate recommendations
        profile = playerp(height, weight, age)
        return jsonify(profile), 200

    except requests.exceptions.HTTPError as err:
        # Handle HTTP errors
        return jsonify({"error": f"Error al obtener datos del jugador: {str(err)}"}), err.response.status_code
    except Exception as e:
        # Handle other exceptions
        print(str(e))
        return jsonify({"error": str(e)}), 500

@app.route('/map', methods=['GET'])
def make_map():

    try:
        with open("players_location.json", "r") as file:
            df = pd.read_json(file)

        # Get the map
        map_html = players_location(df)
        return jsonify({"map": map_html}), 200

    except FileNotFoundError:
        return jsonify({"error": "Archivo no encontrado"}), 404
    except requests.exceptions.RequestException as e:
        print(str(e))
        return jsonify({'error': str(e)}), 400

@app.route('/parity/<cathegory_id>', methods=['GET'])
def group_parity(cathegory_id):
    if cathegory_id == app.config["CATHEGORY_SENIORS_ID"]:
        try:
            with open("parity_seniors.json", "r") as file:
                data = json.load(file)
                data = pd.DataFrame(data)
            plot_json = plotly_plot_parity(data)
            prepare =  jsonify({"data": plot_json["data"], "x-axis":"Grupos", "y-axis":"Indice de paridad", "title":"¿Qué tan parejos estuvieron los grupos de la categoria 50 y más?"})
            return prepare,200
        except FileNotFoundError:
            return jsonify({"error": "Archivo no encontrado"}), 404

    elif cathegory_id == app.config["CATHEGORY_OPEN_ID"]:
        try:
            with open("parity_open.json", "r") as file:
                data = json.load(file)
                data = pd.DataFrame(data)
                #print(data)
            plot_json = plotly_plot_parity(data)
            #print(plot_json)
            prepare =  jsonify({"data": plot_json["data"], "x-axis":"Grupos", "y-axis":"Indice de paridad", "title":"¿Qué tan parejos estuvieron los grupos de la categoria Libre?"})
            return prepare,200
        except FileNotFoundError:
            return jsonify({"error": "Archivo no encontrado"}), 404

    return jsonify({"error": "Categoría no válida"}), 400

@app.route('/points/<cathegory_id>/<group>', methods=['GET'])
def points(cathegory_id, group):

    if cathegory_id == app.config["CATHEGORY_SENIORS_ID"]:
        if group not in ['A', 'B', 'C', 'D']:
            return jsonify({"error": "Grupo no válido"}), 400
        try:
            df = pd.read_csv("data_matches_seniors.csv")
            df_group = extract_group(df, group)
            #print(df_group)
            plot_json = plotly_plot_points(df_group)
            prepare =  jsonify({"data": plot_json["data"], "x-axis":"Jugadores", "y-axis":"Puntos", "title":"Puntos hechos vs puntos recibidos del grupo " + group})
            return prepare,200

        except FileNotFoundError:
            return jsonify({"error": "Archivo no encontrado"}), 404

    elif cathegory_id == app.config["CATHEGORY_OPEN_ID"]:
        if group not in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']:
            return jsonify({"error": "Grupo no válido"}), 400
        try:
            df = pd.read_csv("data_matches_open.csv")
            df_group = extract_group(df, group)
            #print(df_group)
            plot_json = plotly_plot_points(df_group)
            prepare =  jsonify({"data": plot_json["data"], "x-axis":"Jugadores", "y-axis":"Puntos", "title":"Puntos hechos vs puntos recibidos del grupo " + group})
            return prepare,200

        except FileNotFoundError:
            return jsonify({"error": "Archivo no encontrado"}), 404

    return jsonify({"error": "Categoría no válida"}), 400

@app.route('/sinergy/<cathegory_id>', methods=['GET'])
def sinergy(cathegory_id):
    if cathegory_id == app.config["CATHEGORY_SENIORS_ID"]:
        try:
            df_s = pd.read_csv("data_matches_seniors.csv")
            df_s = df_s[['team', 'points_scored', 'points_received']].copy()
            df_s['sinergy'] = (((df_s['points_scored'] - df_s['points_received']+3) / 30) * 100).round(2)
            df_s["sinergy"] = df_s['sinergy'].apply(lambda x: x+0.1 if x == 0 else x)
            df_s['team'] = df_s['team'].apply(lambda x: '/'.join(
            name.strip().split()[0]
            for name in x.strip('[]').split(', ')
            ))
            plot_json = plotly_plot_sinergy(df_s)
            prepare =  jsonify({"data": plot_json["data"], "x-axis":"Parejas", "y-axis":"Sinergia", "title":"Sinergia de los equipos de la categoria 50 y más"})
            return prepare,200
        except FileNotFoundError:
            return jsonify({"error": "Archivo no encontrado"}), 404
    elif cathegory_id == app.config["CATHEGORY_OPEN_ID"]:
        try:
            df_o = pd.read_csv("data_matches_open.csv")
            df_o = df_o[['team', 'points_scored', 'points_received']].copy()
            df_o['sinergy'] = (((df_o['points_scored'] - df_o['points_received']+3) / 30) * 100).round(2)
            df_o["sinergy"] = df_o['sinergy'].apply(lambda x: x+0.1 if x == 0 else x)
            df_o['team'] = df_o['team'].apply(lambda x: '/'.join(
            name.strip().split()[0]
            for name in x.strip('[]').split(', ')
            ))
            plot_json = plotly_plot_sinergy(df_o)
            prepare =  jsonify({"data": plot_json["data"], "x-axis":"Parejas", "y-axis":"Sinergia", "title":"Sinergia de los equipos de la categoria Libre"})
            return prepare,200
        except FileNotFoundError:
            return jsonify({"error": "Archivo no encontrado"}), 404
    return jsonify({"error": "Categoría no válida"}), 400

@app.route('/sinergy_compare', methods=['GET'])
def sinergy_compare():
    try:
        df_s = pd.read_csv("data_matches_seniors.csv")
        df_s = df_s[['team', 'points_scored', 'points_received']].copy()
        df_s['sinergy'] = (((df_s['points_scored'] - df_s['points_received']+3) / 30) * 100).round(2)
        df_s["sinergy"] = df_s['sinergy'].apply(lambda x: x+0.1 if x == 0 else x)
        sinergy_senior = df_s['sinergy'].mean()

        df_o = pd.read_csv("data_matches_open.csv")
        df_o = df_o[['team', 'points_scored', 'points_received']].copy()
        df_o['sinergy'] = (((df_o['points_scored'] - df_o['points_received']+3) / 30) * 100).round(2)
        df_o["sinergy"] = df_o['sinergy'].apply(lambda x: x+0.1 if x == 0 else x)
        sinergy_open = df_o['sinergy'].mean()

        sinergy_group = {
            'sinergy_open': float(sinergy_open),
            'sinergy_senior': float(sinergy_senior)
        }
        sinergy_group["sinergy_open"] = round(sinergy_group["sinergy_open"], 4)
        sinergy_group["sinergy_senior"] = round(sinergy_group["sinergy_senior"], 4)

        plot_json = plotly_plot_sinergy_compare(sinergy_group)
        prepare =  jsonify({"data": plot_json["data"], "x-axis":"Categoría", "y-axis":"Sinergia", "title":"Comparativa de sinergia entre categorias"})
        return prepare,200

    except FileNotFoundError:
        return jsonify({"error": "Archivo no encontrado"}), 404

@app.route('/points_compare', methods=['GET'])
def points_compare():
    try:
        df_s = pd.read_csv("data_matches_seniors.csv")
        points_seniors = df_s['points_scored'].sum()
        points_seniors = points_seniors/4

        df_o = pd.read_csv("data_matches_open.csv")
        points_open = df_o['points_scored'].sum()
        points_open = points_open/8

        points_groups = {
        'points_open': float(points_open),
        'points_seniors': float(points_seniors)
        }
        plot_json = plotly_plot_points_compare(points_groups)
        prepare =  jsonify({"data": plot_json["data"], "x-axis":"Categoría", "y-axis":"Puntos promedio", "title":"Promedio de puntos hechos por categoria"})
        return prepare,200

    except FileNotFoundError:
        return jsonify({"error": "Archivo no encontrado"}), 404

@app.route('/feelings', methods=['GET'])
def feelings():
    try:
        feelings = get_analysys()
        return jsonify({"analysis": feelings}), 200
    except Exception as e:
        print(str(e))
        return jsonify({"error": str(e)}), 500

if __name__ == "__main__":
    app.run(port=3000, debug = True)


code:
group,team_id,team,points_scored,points_received,points_diff
A,0e4ff1f4-19db-4380-a57c-62881b39e146,"[Marco Belmonte, Servando Rincón]",13,27,14
A,ff43b8fd-51e5-47ce-ba80-8d428b61c5b9,"[Juvenal Labra, Oscar Chavez]",13,20,7
A,4e9579ca-4b9e-4018-bb02-5ea047a1938b,"[Alfredo Manriquez, Alberto Chacón]",17,11,6
A,8a9e8890-5e67-4300-8792-c18e4fe123f8,"[Alejandro Guzmán, Rodolfo Pérez]",28,13,15
B,af59fb07-0bc4-4148-8fa0-ab9971c396fe,"[Fernando González, Patricio Correa]",17,25,8
B,4f473639-0ad1-4984-b02a-1861d1c43324,"[Nery Contreras, José Gpe Fierros]",18,19,1
B,c5e42862-c78a-4802-8a07-4394d5b177df,"[Antonio Melendez, Leopoldo Moya]",18,30,12
B,378f5c67-cd7c-4ab3-9b8f-50acf38f1661,"[Carlos Labra, Antonio CBaby]",30,9,21
C,d70aef35-368b-4197-8c9c-cbca64866c10,"[Rafael Aleman, Abraham Aguiñiga]",15,30,15
C,6f288107-8b7c-48b0-b7e7-abc81a974be1,"[Antonio Mendoza, Victor Arellano]",19,29,10
C,5a93fab1-5470-4f4e-b36d-beb875f1a17f,"[Joaquin López, Salvador Farías]",25,10,15
C,d02eebca-fa3d-477c-9a28-c4a4c4e3f86f,"[Javier Zavala, Jorge Zavala]",27,17,10
D,8b7d034d-1a8b-4037-9c95-55ea16a961ce,"[Ricardo Morales, Gustavo Jiménez]",3,20,17
D,499079b5-1054-46e7-8427-315a5593d0b0,"[Refugio Castillo, Ulises Asencio]",13,18,5
D,3a05e116-d143-404c-8523-ba7a57d25ad8,"[Humberto Martínez, Marco Martínez]",26,14,12
D,6b7214ae-6542-42c9-81df-f19736a75fea,"[David Godínez, Alfredo Rocha]",27,17,10


display_name: Python 3

cell_type: code

source:
import folium
from folium.plugins import HeatMap
import pandas as pd
import time
import requests
import json

source: params = {"q": f"{city}, México", "format": "json", "limit": 1}

source: headers = {"User-Agent": "torneo_gobierno_irapuato_frontenis_v1.0 (contacto@irapuato.gob.mx)"}

source: response = requests.get(url, params=params, headers=headers, timeout=15)

source: response.raise_for_status()  # launch an exception if the request was unsuccesful

source: return [float(data[0]["lat"]), float(data[0]["lon"])] if data else None

source: except requests.exceptions.RequestException as e:

source: # Guardar el DataFrame como un archivo JSON con codificación UTF-8

source: return print("Json guardado correctamente")

source: response = requests.post('https://api-x90k.onrender.com/v1/auth/login', json=payload)

source: except requests.exceptions.HTTPError as err:

source: print("Se agotó el tiempo de la solicitud")

source: response = requests.get('https://api-x90k.onrender.com/v1/players', headers=headers)

source: # Verificar el código de estado de la respuesta

source: # Verificar si la respuesta tiene contenido

source: response = requests.get("https://api-x90k.onrender.com/v1/players", headers=headers, params=params)

source: #make a df with two columns city and number of players

source: cities_df = cities_df.sort_values(by="players", ascending=False)

source: cities_df.loc[cities_df['city']=='San Luis Potosí', 'city'] = 'San Luis Potosi'

source: cities_df.loc[cities_df['city']=='Penjámo Gto', 'city'] = 'Pénjamo'

text/plain:
city  players
7            Irapuato       40
10            Morelia       11
12            Pénjamo        7
3              Celaya        6
11           Moroleón        5
13          Querétaro        5
5            Cortazar        4
4    Ciudad de México        3
14    San Luis Potosi        3
0              Aculco        2
1      Apaseo el Alto        2
2    Apaseo el Grande        2
8   Ixtapan de la Sal        2
9           Jilotepec        2
6         Guadalajara        1
15             Zacapu        1

text/html: title="Convert this dataframe to an interactive table."

text/html: <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960">

text/html: box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);

text/html: filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));

text/html: google.colab.kernel.accessAllowed ? 'block' : 'none';

text/html: async function convertToInteractive(key) {

text/html: const element = document.querySelector('#df-ed0e19de-60bb-46ee-9363-8753a5942e1c');

text/html: const docLinkHtml = 'Like what you see? Visit the ' +

text/html: + ' to learn more about interactive tables.';

text/html: const docLink = document.createElement('div');

text/html: <svg xmlns="http://www.w3.org/2000/svg" height="24px"viewBox="0 0 24 24"

text/html: document.querySelector('#' + key + ' button');

text/html: quickchartButtonEl.disabled = true;  // To prevent multiple clicks.

text/html: const charts = await google.colab.kernel.invokeFunction(

text/html: console.error('Error during call to suggestCharts:', error);

text/html: title="Generate code using this dataframe."

summary:
{
  "name": "cities_df",
  "rows": 16,
  "fields": [
    {
      "column": "city",
      "properties": {
        "dtype": "string",
        "num_unique_values": 16,
        "samples": [
          "Irapuato",
          "Morelia",
          "Quer\u00e9taro"
        ],
        "semantic_type": "",
        "description": ""
      }
    },
    {
      "column": "players",
      "properties": {
        "dtype": "number",
        "std": 9,
        "min": 1,
        "max": 40,
        "num_unique_values": 9,
        "samples": [
          2,
          11,
          4
        ],
        "semantic_type": "",
        "description": ""
      }
    }
  ]
}

stdout

text: Json guardado correctamente


code:
blinker==1.9.0
branca==0.8.1
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
contourpy==1.3.1
cycler==0.12.1
decorator==5.2.1
executing==2.2.0
Flask==3.1.0
flask-cors==5.0.1
folium==0.19.5
fonttools==4.56.0
gunicorn==23.0.0
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.5
kiwisolver==1.4.8
MarkupSafe==3.0.2
matplotlib==3.10.1
narwhals==1.32.0
numpy==2.2.3
packaging==24.2
pandas==2.2.3
pillow==11.1.0
platformdirs==4.3.6
plotly==6.0.1
Pygments==2.19.1
pyparsing==3.2.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
pytz==2025.1
requests==2.32.3
seaborn==0.13.2
six==1.17.0
tzdata==2025.1
urllib3==2.3.0
Werkzeug==3.1.3
xyzservices==2025.1.0


code:
��asttokens==3.0.0

blinker==1.9.0

branca==0.8.1

certifi==2025.1.31

charset-normalizer==3.4.1

click==8.1.8

colorama==0.4.6

comm==0.2.2

contourpy==1.3.1

cycler==0.12.1

debugpy==1.8.13

decorator==5.2.1

executing==2.2.0

Flask==3.1.0

flask-cors==5.0.1

folium==0.19.5

fonttools==4.56.0

gunicorn==23.0.0

idna==3.10

ipykernel==6.29.5

ipython==9.0.1

ipython_pygments_lexers==1.1.1

itsdangerous==2.2.0

jedi==0.19.2

Jinja2==3.1.5

jupyter_client==8.6.3

jupyter_core==5.7.2

kiwisolver==1.4.8

MarkupSafe==3.0.2

matplotlib==3.10.1

matplotlib-inline==0.1.7

narwhals==1.32.0

nest-asyncio==1.6.0

numpy==2.2.3

packaging==24.2

pandas==2.2.3

parso==0.8.4

pillow==11.1.0

platformdirs==4.3.6

plotly==6.0.1

prompt_toolkit==3.0.50

psutil==7.0.0

pure_eval==0.2.3

Pygments==2.19.1

pyparsing==3.2.1

python-dateutil==2.9.0.post0

python-dotenv==1.0.1

pytz==2025.1

pywin32==308

pyzmq==26.2.1

requests==2.32.3

seaborn==0.13.2

six==1.17.0

stack-data==0.6.3

tornado==6.4.2

traitlets==5.14.3

tzdata==2025.1

urllib3==2.3.0

wcwidth==0.2.13

Werkzeug==3.1.3

xyzservices==2025.1.0


code:
<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Torneo de las Fresas 2025</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">

                API de Análisis de Datos y Machine Learning

                Torneo de las Fresas 2025

🧠 Modelos IA

Análisis profundo de los datos.

📊 Datos precisos

Conociendo de cerca la información.

🏅 Player Analytics

Rendimiento, sinergia y métricas clave.

                    In Data We Trust


code:
import os
#Clean my requiremetns.txt file to avoid anything related with Jupyter Notebook and ipykernel
excluded_packages = {
    "ipykernel", "ipython", "jupyter", "debugpy", "pyzmq",
    "matplotlib-inline", "prompt_toolkit", "traitlets",
    "pywin32", "tornado", "colorama", "asttokens", "comm",
    "wcwidth", "stack-data", "nest-asyncio", "parso", "psutil",
    "pure_eval", "jedi", "jupyter_client", "jupyter_core", "ipython_pygments_lexers"
}

requirements_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../requirements_local.txt"))
output_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../requirements.txt"))

with open(requirements_path, "r", encoding="utf-16") as f:
    lines = f.readlines()

clean_lines = [
    line for line in lines
    if line.split("==")[0].lower() not in excluded_packages  # Compara solo el nombre
]

with open(output_path, "w") as f:
    f.writelines(clean_lines)